Initial commit
This commit is contained in:
commit
b344a74a72
45 changed files with 1232 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dev.db
|
||||||
|
.env
|
88
README.md
Normal file
88
README.md
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# vike-stack
|
||||||
|
|
||||||
|
- Base
|
||||||
|
- [Vike](https://vike.dev)
|
||||||
|
- [Hono](https://hono.dev)
|
||||||
|
- [React](https://react.dev/learn)
|
||||||
|
- [Telefunc](https://telefunc.com)
|
||||||
|
- Auth
|
||||||
|
- [Lucia Auth](https://lucia-auth.com)
|
||||||
|
- @lucia-auth/adapter-prisma
|
||||||
|
- [Prisma ORM](https://www.prisma.io)
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
### Setup _Prisma_
|
||||||
|
|
||||||
|
Run the following command once:
|
||||||
|
|
||||||
|
1. Run `npx prisma init`
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
2. Run `npx prisma db pull` to turn your database schema into a Prisma schema.
|
||||||
|
3. Run `npx prisma generate` to generate the Prisma Client.
|
||||||
|
4. Run `npx prisma db push` to push scheme to database.
|
||||||
|
5. Run `npx prisma migrate dev` to regenerate schema (if scheme change)
|
||||||
|
|
||||||
|
### Example .env file
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# postgres:
|
||||||
|
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
|
||||||
|
|
||||||
|
# sqlite
|
||||||
|
DATABASE_URL="file:./dev.db"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
With one simple alias `~/`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Input } from "~/components/Input";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```
|
||||||
|
# yarn dev
|
||||||
|
or
|
||||||
|
# bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
# About this app
|
||||||
|
|
||||||
|
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
|
||||||
|
|
||||||
|
### `/pages/+config.ts`
|
||||||
|
|
||||||
|
Such files are [the interface](https://vike.dev/config) between Vike and your code. It defines:
|
||||||
|
|
||||||
|
- A default [`<Layout>` component](https://vike.dev/Layout) (that wraps your [`<Page>` components](https://vike.dev/Page)).
|
||||||
|
- A default [`title`](https://vike.dev/head).
|
||||||
|
- Default [`<head>` tags](https://vike.dev/head).
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
|
||||||
|
[Vike's built-in router](https://vike.dev/routing) lets you choose between:
|
||||||
|
|
||||||
|
- [Filesystem Routing](https://vike.dev/filesystem-routing) (the URL of a page is determined based on where its `+Page.jsx` file is located on the filesystem)
|
||||||
|
- [Route Strings](https://vike.dev/route-string)
|
||||||
|
- [Route Functions](https://vike.dev/route-function)
|
||||||
|
|
||||||
|
### `/pages/_error/+Page.jsx`
|
||||||
|
|
||||||
|
The [error page](https://vike.dev/error-page) which is rendered when errors occur.
|
||||||
|
|
||||||
|
### `/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`
|
||||||
|
|
||||||
|
The [`onPageTransitionStart()` hook](https://vike.dev/onPageTransitionStart), together with [`onPageTransitionEnd()`](https://vike.dev/onPageTransitionEnd), enables you to implement page transition animations.
|
||||||
|
|
||||||
|
### SSR
|
||||||
|
|
||||||
|
SSR is enabled by default. You can [disable it](https://vike.dev/ssr) for all your pages or only for some pages.
|
||||||
|
|
||||||
|
### HTML Streaming
|
||||||
|
|
||||||
|
You can enable/disable [HTML streaming](https://vike.dev/streaming) for all your pages, or only for some pages while still using it for others.
|
36
assets/logo.svg
Normal file
36
assets/logo.svg
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="175" height="175" fill="none" version="1.1" viewBox="0 0 175 175" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<metadata>
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="linearGradient880" x1="108.64" x2="115.51" y1="88.726" y2="136.2" gradientTransform="matrix(1.0498 0 0 1.0498 -2.9171 -2.9658)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#ffea83" offset="0"/>
|
||||||
|
<stop stop-color="#FFDD35" offset=".083333"/>
|
||||||
|
<stop stop-color="#FFA800" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear" x1="48.975" x2="61.299" y1="3.9232" y2="158.04" gradientTransform="translate(-2.832e-5)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFEA83" offset="0"/>
|
||||||
|
<stop stop-color="#FFDD35" offset=".083333"/>
|
||||||
|
<stop stop-color="#FFA800" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint0_linear-6" x1="-1.4492" x2="116.62" y1="-5.8123" y2="137.08" gradientTransform="translate(-2.832e-5)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#41D1FF" offset="0"/>
|
||||||
|
<stop stop-color="#BD34FE" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="87.5" cy="87.5" r="87.5" fill="#c4c4c4"/>
|
||||||
|
<circle cx="87.5" cy="87.5" r="87.5" fill="url(#paint0_linear-6)"/>
|
||||||
|
<g transform="translate(632.92 54.355)" fill="#d38787" stroke-width="1.0614">
|
||||||
|
<path d="m-549.75 68.457c-5.7533-3.1217-6.1166-5.2295-6.1166-35.489 0-30.458 0.35464-32.448 6.3339-35.54 3.9943-2.0655 24.279-2.2805 26.735-0.28333 0.89718 0.72974 6.7203 6.6637 12.94 13.187l11.309 11.86v19.575c0 18.473-0.12956 19.74-2.3011 22.5-4.0223 5.1136-7.558 5.8565-27.65 5.8099-14.15-0.03287-19.008-0.40294-21.25-1.6191zm42.473-6.3594c2.27-1.59 2.359-2.2909 2.359-18.575v-16.923h-6.9521c-12.443 0-16.4-4.0845-16.4-16.93v-7.4828h-8.9464c-6.7178 0-9.3619 0.41549-10.614 1.668-2.5031 2.5031-2.5031 55.724 0 58.228 2.4502 2.4502 37.058 2.4636 40.553 0.01609zm-1.8867-42.165c0-0.16422-2.8659-3.1346-6.3686-6.6008l-6.3686-6.3022v4.9328c0 6.3185 1.8955 8.2687 8.0366 8.2687 2.5854 0 4.7007-0.13434 4.7007-0.29859zm-57.57 44.279c-5.6185-3.0486-6.1166-5.593-6.1166-31.243 0-18.891 0.31331-24.063 1.6101-26.571 1.809-3.4981 6.5048-6.3339 10.489-6.3339 2.4847 0 2.5814 0.19984 1.541 3.1843-0.61054 1.7514-1.7457 3.1843-2.5226 3.1843-0.77686 0-2.1631 0.75059-3.0805 1.668-2.4923 2.4923-2.4923 47.244 0 49.736 0.91739 0.9174 2.3036 1.668 3.0805 1.668 0.77688 0 1.912 1.4329 2.5226 3.1843 1.0562 3.0298 0.97108 3.1822-1.7537 3.1418-1.575-0.02331-4.1713-0.75194-5.7694-1.6191zm-16.983-4.2458c-5.4392-2.9512-6.1166-5.9415-6.1166-26.997 0-15.096 0.345-19.878 1.6101-22.325 1.7476-3.3796 6.4758-6.3339 10.137-6.3339 1.8666 0 2.1789 0.44955 1.6594 2.3882-0.35184 1.3135-0.64655 2.7465-0.65453 3.1843-8e-3 0.43784-0.69682 0.79608-1.5308 0.79608-0.83399 0-2.2669 0.75059-3.1843 1.668-2.4767 2.4767-2.4767 38.768 0 41.244 0.91741 0.91739 2.2946 1.668 3.0605 1.668 1.196 0 2.6402 2.995 2.6871 5.5726 0.0241 1.3294-4.5804 0.80962-7.6676-0.8655z" style="mix-blend-mode:lighten"/>
|
||||||
|
<path d="m-552.2 68.911c-5.7533-3.1217-6.1166-5.2295-6.1166-35.489 0-30.458 0.35463-32.448 6.3339-35.54 3.9943-2.0655 24.279-2.2805 26.735-0.28333 0.89718 0.72974 6.7203 6.6637 12.94 13.187l11.309 11.86v19.575c0 18.473-0.12957 19.74-2.3011 22.5-4.0223 5.1136-7.558 5.8565-27.65 5.8099-14.15-0.03287-19.008-0.40294-21.25-1.6191zm42.473-6.3594c2.27-1.59 2.359-2.2909 2.359-18.575v-16.923h-6.952c-12.443 0-16.4-4.0845-16.4-16.93v-7.4828h-8.9464c-6.7179 0-9.3619 0.41549-10.614 1.668-2.5031 2.5031-2.5031 55.724 0 58.228 2.4502 2.4502 37.058 2.4636 40.553 0.01609zm-1.8867-42.165c0-0.16422-2.8659-3.1346-6.3686-6.6008l-6.3686-6.3022v4.9328c0 6.3185 1.8955 8.2688 8.0366 8.2688 2.5854 0 4.7007-0.13434 4.7007-0.29859zm-57.57 44.279c-5.6185-3.0486-6.1166-5.593-6.1166-31.243 0-18.891 0.31331-24.063 1.6101-26.571 1.809-3.4981 6.5048-6.3339 10.489-6.3339 2.4847 0 2.5814 0.19984 1.541 3.1843-0.61054 1.7514-1.7457 3.1843-2.5226 3.1843-0.77687 0-2.1631 0.75059-3.0805 1.668-2.4923 2.4923-2.4923 47.244 0 49.736 0.91741 0.91739 2.3036 1.668 3.0805 1.668 0.77686 0 1.912 1.4329 2.5226 3.1843 1.0562 3.0298 0.97107 3.1822-1.7537 3.1418-1.575-0.02331-4.1713-0.75194-5.7694-1.6191zm-16.983-4.2458c-5.4392-2.9512-6.1166-5.9415-6.1166-26.997 0-15.096 0.34502-19.878 1.6101-22.325 1.7476-3.3796 6.4758-6.3339 10.137-6.3339 1.8666 0 2.1789 0.44955 1.6594 2.3882-0.35182 1.3135-0.64653 2.7465-0.65452 3.1843-8e-3 0.43784-0.69683 0.79608-1.5308 0.79608-0.83397 0-2.2669 0.75059-3.1843 1.668-2.4767 2.4767-2.4767 38.768 0 41.245 0.9174 0.91739 2.2946 1.668 3.0605 1.668 1.196 0 2.6402 2.995 2.6871 5.5726 0.0241 1.3294-4.5804 0.80962-7.6676-0.8655z" fill-opacity=".47466" style="mix-blend-mode:lighten"/>
|
||||||
|
</g>
|
||||||
|
<path d="m128.48 88.913-24.027 4.6784c-0.39475 0.07685-0.68766 0.40944-0.71076 0.80849l-1.4782 24.805c-0.0347 0.58371 0.50497 1.0372 1.0792 0.90602l6.6886-1.5338c0.62676-0.14383 1.1916 0.40419 1.0635 1.0299l-1.9874 9.6702c-0.13438 0.65091 0.48084 1.2073 1.1202 1.0142l4.1322-1.2472c0.64041-0.19317 1.2556 0.36535 1.1202 1.0162l-3.158 15.191c-0.19842 0.95011 1.074 1.4677 1.6042 0.653l0.35485-0.54382 19.578-38.827c0.32755-0.64985-0.23727-1.391-0.95641-1.2535l-6.8849 1.3207c-0.6467 0.12389-1.1979-0.47453-1.0152-1.1034l4.4944-15.482c0.18266-0.63012-0.36955-1.2295-1.0173-1.1034z" fill="url(#linearGradient880)" stroke-width="1.0498"/>
|
||||||
|
<rect x="3" y="3" width="169" height="169" rx="84.5" stroke="url(#paint2_linear)" stroke-width="6" style="mix-blend-mode:soft-light"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.7 KiB |
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
15
components/Input.tsx
Normal file
15
components/Input.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React, { InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Input({ id, ...props }: InputProps) {
|
||||||
|
return (
|
||||||
|
<label style={{ display: "block" }}>
|
||||||
|
<span style={{ fontSize: "0.91em" }}>{id}</span>
|
||||||
|
<br />
|
||||||
|
<input id={id} size={20} {...props}></input>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
13
components/Link.tsx
Normal file
13
components/Link.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react";
|
||||||
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
|
|
||||||
|
export function Link({ href, children }: { href: string; children: string }) {
|
||||||
|
const pageContext = usePageContext();
|
||||||
|
const { urlPathname } = pageContext;
|
||||||
|
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
|
||||||
|
return (
|
||||||
|
<a href={href} className={isActive ? "is-active" : undefined}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
12
database/todoItems.ts
Normal file
12
database/todoItems.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
type TodoItem = { text: string };
|
||||||
|
const todoItems: TodoItem[] = [];
|
||||||
|
init();
|
||||||
|
|
||||||
|
// Initial data
|
||||||
|
function init() {
|
||||||
|
todoItems.push({ text: "Buy milk" });
|
||||||
|
todoItems.push({ text: "Buy strawberries" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { todoItems };
|
||||||
|
export type { TodoItem };
|
80
hono-entry.ts
Normal file
80
hono-entry.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { serve } from "@hono/node-server";
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { compress } from "hono/compress";
|
||||||
|
import { poweredBy } from "hono/powered-by";
|
||||||
|
import { telefunc } from "telefunc";
|
||||||
|
import { renderPage } from "vike/server";
|
||||||
|
|
||||||
|
import authRoute from "~/lib/auth";
|
||||||
|
|
||||||
|
import type { Session, User } from "lucia";
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
||||||
|
|
||||||
|
// const app = new Hono();
|
||||||
|
|
||||||
|
const app = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
user: User | null;
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
app.use(poweredBy());
|
||||||
|
app.use(compress());
|
||||||
|
|
||||||
|
app.route("/", authRoute);
|
||||||
|
|
||||||
|
if (isProduction) {
|
||||||
|
app.use(
|
||||||
|
"/*",
|
||||||
|
serveStatic({
|
||||||
|
root: `dist/client/`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post("/_telefunc", async (c) => {
|
||||||
|
const httpResponse = await telefunc({
|
||||||
|
url: c.req.url.toString(),
|
||||||
|
method: c.req.method,
|
||||||
|
body: await c.req.text(),
|
||||||
|
context: c,
|
||||||
|
});
|
||||||
|
const { body, statusCode, contentType } = httpResponse;
|
||||||
|
|
||||||
|
c.status(statusCode);
|
||||||
|
c.header("Content-Type", contentType);
|
||||||
|
|
||||||
|
return c.body(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.all("*", async (c, next) => {
|
||||||
|
const pageContextInit = {
|
||||||
|
urlOriginal: c.req.url,
|
||||||
|
auth: c.get("user"),
|
||||||
|
};
|
||||||
|
const pageContext = await renderPage(pageContextInit);
|
||||||
|
const { httpResponse } = pageContext;
|
||||||
|
if (!httpResponse) {
|
||||||
|
return next();
|
||||||
|
} else {
|
||||||
|
const { body, statusCode, headers } = httpResponse;
|
||||||
|
headers.forEach(([name, value]) => c.header(name, value));
|
||||||
|
c.status(statusCode);
|
||||||
|
|
||||||
|
return c.body(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isProduction) {
|
||||||
|
console.log(`Server listening on http://localhost:${port}`);
|
||||||
|
serve({
|
||||||
|
fetch: app.fetch,
|
||||||
|
port: port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default app;
|
14
layouts/HeadDefault.tsx
Normal file
14
layouts/HeadDefault.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from "react";
|
||||||
|
import logoUrl from "../assets/logo.svg";
|
||||||
|
|
||||||
|
// Default <head> (can be overridden by pages)
|
||||||
|
|
||||||
|
export default function HeadDefault() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="Demo showcasing Vike" />
|
||||||
|
<link rel="icon" href={logoUrl} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
88
layouts/LayoutDefault.tsx
Normal file
88
layouts/LayoutDefault.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import logoUrl from "../assets/logo.svg";
|
||||||
|
import { Link } from "../components/Link.js";
|
||||||
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
|
|
||||||
|
export default function LayoutDefault({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const pageContext = usePageContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
maxWidth: 900,
|
||||||
|
margin: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sidebar>
|
||||||
|
<Logo />
|
||||||
|
<Link href="/">Welcome</Link>
|
||||||
|
<Link href="/todo">Todo (telefunc)</Link>
|
||||||
|
<Link href="/star-wars">Data Fetching</Link>
|
||||||
|
|
||||||
|
{pageContext.auth ? (
|
||||||
|
<Link href="/account">Account</Link>
|
||||||
|
) : (
|
||||||
|
<Link href="/auth">Auth</Link>
|
||||||
|
)}
|
||||||
|
</Sidebar>
|
||||||
|
<Content>{children}</Content>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="sidebar"
|
||||||
|
style={{
|
||||||
|
padding: 20,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
lineHeight: "1.8em",
|
||||||
|
borderRight: "2px solid #eee",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div id="page-container">
|
||||||
|
<div
|
||||||
|
id="page-content"
|
||||||
|
style={{
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 50,
|
||||||
|
minHeight: "100vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Logo() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a href="/">
|
||||||
|
<img src={logoUrl} height={64} width={64} alt="logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
29
layouts/style.css
Normal file
29
layouts/style.css
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/* Links */
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
#sidebar a {
|
||||||
|
padding: 2px 10px;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
#sidebar a.is-active {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Transition Animation */
|
||||||
|
#page-content {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
body.page-is-transitioning #page-content {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
148
lib/auth.ts
Normal file
148
lib/auth.ts
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import { Context, Hono } from "hono";
|
||||||
|
import { getCookie, setCookie } from "hono/cookie";
|
||||||
|
import type { User, Session } from "lucia";
|
||||||
|
import { generateId } from "lucia";
|
||||||
|
import { Argon2id } from "oslo/password";
|
||||||
|
import { prisma } from "./prisma.js";
|
||||||
|
import { lucia } from "./lucia.js";
|
||||||
|
import { CookieOptions } from "hono/utils/cookie";
|
||||||
|
|
||||||
|
const app = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
user: User | null;
|
||||||
|
session: Session | null;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
app.use("*", async (c, next) => {
|
||||||
|
const sessionId = getCookie(c, lucia.sessionCookieName) ?? null;
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
c.set("user", null);
|
||||||
|
c.set("session", null);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session, user } = await lucia.validateSession(sessionId);
|
||||||
|
|
||||||
|
if (session && session.fresh) {
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
setAuthCookie(c, sessionCookie);
|
||||||
|
}
|
||||||
|
if (!session) {
|
||||||
|
const sessionCookie = lucia.createBlankSessionCookie();
|
||||||
|
setAuthCookie(c, sessionCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.set("user", user);
|
||||||
|
c.set("session", session);
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const handler = app
|
||||||
|
.post("/api/auth/login", async (c) => {
|
||||||
|
const { email, password } = await c.req.json();
|
||||||
|
|
||||||
|
const formDataRaw = {
|
||||||
|
email: email as string,
|
||||||
|
password: password as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: formDataRaw.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return c.text("Incorrect email or password", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPassword = await new Argon2id().verify(
|
||||||
|
user.hashedPassword,
|
||||||
|
formDataRaw.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validPassword) {
|
||||||
|
return c.text("Incorrect email or password", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await lucia.createSession(user.id, {});
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
|
||||||
|
setAuthCookie(c, sessionCookie);
|
||||||
|
|
||||||
|
return c.json("ok");
|
||||||
|
} catch (error) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/api/auth/register", async (c) => {
|
||||||
|
const { firstName, lastName, email, password, confirmPassword } =
|
||||||
|
await c.req.json();
|
||||||
|
|
||||||
|
const formDataRaw = {
|
||||||
|
firstName: firstName as string,
|
||||||
|
lastName: lastName as string,
|
||||||
|
email: email as string,
|
||||||
|
password: password as string,
|
||||||
|
confirmPassword: confirmPassword as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formDataRaw.password !== formDataRaw.confirmPassword) {
|
||||||
|
return c.text("Passwords do not match", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hashedPassword = await new Argon2id().hash(formDataRaw.password);
|
||||||
|
const userId = generateId(15);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: userId,
|
||||||
|
firstName: formDataRaw.firstName,
|
||||||
|
lastName: formDataRaw.lastName,
|
||||||
|
email: formDataRaw.email,
|
||||||
|
hashedPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.text("Something went wrong, try again", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await lucia.createSession(userId, {});
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
|
||||||
|
setAuthCookie(c, sessionCookie);
|
||||||
|
|
||||||
|
return c.json("ok");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/api/auth/logout", async (c) => {
|
||||||
|
if (c.get("user") == null) return;
|
||||||
|
|
||||||
|
await lucia.invalidateSession(c.get("session")?.id ?? "");
|
||||||
|
|
||||||
|
const sessionCookie = lucia.createBlankSessionCookie();
|
||||||
|
setAuthCookie(c, sessionCookie);
|
||||||
|
|
||||||
|
return c.json("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
export type AuthRPCType = typeof handler;
|
||||||
|
|
||||||
|
const setAuthCookie = (
|
||||||
|
c: Context,
|
||||||
|
sessionCookie: { name: string; value: string; attributes: CookieOptions }
|
||||||
|
) => {
|
||||||
|
setCookie(
|
||||||
|
c,
|
||||||
|
sessionCookie.name,
|
||||||
|
sessionCookie.value,
|
||||||
|
sessionCookie.attributes
|
||||||
|
);
|
||||||
|
};
|
34
lib/lucia.ts
Normal file
34
lib/lucia.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Lucia } from "lucia";
|
||||||
|
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||||
|
import { prisma } from "./prisma";
|
||||||
|
|
||||||
|
const adapter = new PrismaAdapter(prisma.session, prisma.user);
|
||||||
|
|
||||||
|
export const lucia = new Lucia(adapter, {
|
||||||
|
sessionCookie: {
|
||||||
|
// this sets cookies with super long expiration
|
||||||
|
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
|
||||||
|
expires: true,
|
||||||
|
attributes: {
|
||||||
|
// set to `true` when using HTTPS
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getUserAttributes: (attributes) => {
|
||||||
|
return {
|
||||||
|
// attributes has the type of DatabaseUserAttributes
|
||||||
|
email: attributes.email,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
declare module "lucia" {
|
||||||
|
interface Register {
|
||||||
|
Lucia: typeof lucia;
|
||||||
|
DatabaseUserAttributes: DatabaseUserAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatabaseUserAttributes {
|
||||||
|
email: string;
|
||||||
|
}
|
5
lib/prisma.ts
Normal file
5
lib/prisma.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// import { PrismaClient } from '@prisma/client/edge'
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export { prisma };
|
43
package.json
Normal file
43
package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "my-app",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "NODE_ENV=production tsx ./hono-entry.ts",
|
||||||
|
"prisma:studio": "prisma studio",
|
||||||
|
"prisma:generate": "prisma generate"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"@hono/vite-dev-server": "^0.12.1",
|
||||||
|
"@types/node": "^18.19.14",
|
||||||
|
"prisma": "^5.14.0",
|
||||||
|
"@types/react": "^18.3.2",
|
||||||
|
"@types/react-dom": "^18.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.11.1",
|
||||||
|
"@lucia-auth/adapter-prisma": "^4.0.1",
|
||||||
|
"@prisma/client": "^5.14.0",
|
||||||
|
"@vite-plugin-vercel/vike": "^6.0.1",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"better-sqlite3": "^10.0.0",
|
||||||
|
"cross-fetch": "^4.0.0",
|
||||||
|
"hono": "^4.3.8",
|
||||||
|
"lucia": "^3.2.0",
|
||||||
|
"oslo": "^1.2.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"telefunc": "^0.1.72",
|
||||||
|
"tsx": "^4.10.5",
|
||||||
|
"vike": "^0.4.171",
|
||||||
|
"vike-react": "^0.4.10",
|
||||||
|
"vite": "^5.2.11",
|
||||||
|
"vite-plugin-vercel": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
14
pages/+config.ts
Normal file
14
pages/+config.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import vikeReact from "vike-react/config";
|
||||||
|
import type { Config } from "vike/types";
|
||||||
|
import Head from "../layouts/HeadDefault.js";
|
||||||
|
import Layout from "../layouts/LayoutDefault.js";
|
||||||
|
|
||||||
|
// Default config (can be overridden by pages)
|
||||||
|
export default {
|
||||||
|
Layout,
|
||||||
|
Head,
|
||||||
|
// <title>
|
||||||
|
title: "My Vike App",
|
||||||
|
extends: vikeReact,
|
||||||
|
passToClient: ["auth"],
|
||||||
|
} satisfies Config;
|
6
pages/+onPageTransitionEnd.ts
Normal file
6
pages/+onPageTransitionEnd.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import type { OnPageTransitionEndAsync } from "vike/types";
|
||||||
|
|
||||||
|
export const onPageTransitionEnd: OnPageTransitionEndAsync = async () => {
|
||||||
|
console.log("Page transition end");
|
||||||
|
document.querySelector("body")?.classList.remove("page-is-transitioning");
|
||||||
|
};
|
6
pages/+onPageTransitionStart.ts
Normal file
6
pages/+onPageTransitionStart.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import type { OnPageTransitionStartAsync } from "vike/types";
|
||||||
|
|
||||||
|
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||||
|
console.log("Page transition start");
|
||||||
|
document.querySelector("body")?.classList.add("page-is-transitioning");
|
||||||
|
};
|
12
pages/PageContext.ts
Normal file
12
pages/PageContext.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { User } from "lucia";
|
||||||
|
|
||||||
|
// https://vike.dev/pageContext#typescript
|
||||||
|
declare global {
|
||||||
|
namespace Vike {
|
||||||
|
interface PageContext {
|
||||||
|
auth: User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
20
pages/_error/+Page.tsx
Normal file
20
pages/_error/+Page.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from "react";
|
||||||
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { is404 } = usePageContext();
|
||||||
|
if (is404) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>404 Page Not Found</h1>
|
||||||
|
<p>This page could not be found.</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>500 Internal Server Error</h1>
|
||||||
|
<p>Something went wrong.</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
31
pages/account/+Page.tsx
Normal file
31
pages/account/+Page.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
import { reload } from "vike/client/router";
|
||||||
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
|
import { hc } from "hono/client";
|
||||||
|
|
||||||
|
import type { AuthRPCType } from "~/lib/auth";
|
||||||
|
import User from "./User";
|
||||||
|
|
||||||
|
function Page() {
|
||||||
|
const client = hc<AuthRPCType>("/");
|
||||||
|
const session = usePageContext().auth ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Account</h1>
|
||||||
|
<button
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await client.api.auth.logout.$post();
|
||||||
|
await reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||||
|
|
||||||
|
<User id={session.id} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
12
pages/account/+guard.ts
Normal file
12
pages/account/+guard.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export { guard };
|
||||||
|
|
||||||
|
import { redirect } from "vike/abort";
|
||||||
|
import type { GuardAsync } from "vike/types";
|
||||||
|
|
||||||
|
const guard: GuardAsync = async (pageContext): ReturnType<GuardAsync> => {
|
||||||
|
const session = pageContext.auth;
|
||||||
|
|
||||||
|
if (session == undefined) {
|
||||||
|
throw redirect("/auth");
|
||||||
|
}
|
||||||
|
};
|
16
pages/account/User.telefunc.ts
Normal file
16
pages/account/User.telefunc.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { prisma } from "~/lib/prisma";
|
||||||
|
|
||||||
|
export const onGetUser = async ({ id }: { id: string }) => {
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
20
pages/account/User.tsx
Normal file
20
pages/account/User.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { onGetUser } from "./User.telefunc";
|
||||||
|
|
||||||
|
const User = ({ id }: { id: string }) => {
|
||||||
|
const [user, setUser] = useState<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onGetUser({ id }).then((user) => {
|
||||||
|
setUser(user);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default User;
|
16
pages/auth/+Page.tsx
Normal file
16
pages/auth/+Page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Link } from "~/components/Link";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<Link href="/auth/sign-in">Sign in</Link>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Link href="/auth/sign-up">Sign up</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
66
pages/auth/sign-in/+Page.tsx
Normal file
66
pages/auth/sign-in/+Page.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { navigate } from "vike/client/router";
|
||||||
|
import { hc } from "hono/client";
|
||||||
|
|
||||||
|
import type { AuthRPCType } from "~/lib/auth";
|
||||||
|
import { Input } from "~/components/Input";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
const client = hc<AuthRPCType>("/");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.api.auth.login.$post({
|
||||||
|
json: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await navigate("/account");
|
||||||
|
} else {
|
||||||
|
setError(await response.text());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Something went wrong.");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Sign In Page</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div id="validation" style={{ color: "#f00" }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
92
pages/auth/sign-up/+Page.tsx
Normal file
92
pages/auth/sign-up/+Page.tsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { navigate } from "vike/client/router";
|
||||||
|
import { hc } from "hono/client";
|
||||||
|
|
||||||
|
import type { AuthRPCType } from "~/lib/auth";
|
||||||
|
import { Input } from "~/components/Input";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
const client = hc<AuthRPCType>("/");
|
||||||
|
const [firstName, setFirstName] = useState("");
|
||||||
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.api.auth.register.$post({
|
||||||
|
json: {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await navigate("/account");
|
||||||
|
} else {
|
||||||
|
setError(await response.text());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Something went wrong.");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Sign Up Page</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
|
type="text"
|
||||||
|
placeholder="First Name"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="lastName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div id="validation" style={{ color: "#f00" }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
17
pages/index/+Page.tsx
Normal file
17
pages/index/+Page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Counter } from "./Counter.js";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>My Vike app</h1>
|
||||||
|
This page is:
|
||||||
|
<ul>
|
||||||
|
<li>Rendered to HTML.</li>
|
||||||
|
<li>
|
||||||
|
Interactive. <Counter />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
pages/index/Counter.tsx
Normal file
11
pages/index/Counter.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
export function Counter() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={() => setCount((count) => count + 1)}>
|
||||||
|
Counter {count}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
17
pages/star-wars/@id/+Page.tsx
Normal file
17
pages/star-wars/@id/+Page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useData } from "vike-react/useData";
|
||||||
|
import type { Data } from "./+data.js";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const movie = useData<Data>();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{movie.title}</h1>
|
||||||
|
Release Date: {movie.release_date}
|
||||||
|
<br />
|
||||||
|
Director: {movie.director}
|
||||||
|
<br />
|
||||||
|
Producer: {movie.producer}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
22
pages/star-wars/@id/+data.ts
Normal file
22
pages/star-wars/@id/+data.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// https://vike.dev/data
|
||||||
|
|
||||||
|
import fetch from "cross-fetch";
|
||||||
|
import type { PageContextServer } from "vike/types";
|
||||||
|
import type { MovieDetails } from "../types.js";
|
||||||
|
|
||||||
|
export type Data = Awaited<ReturnType<typeof data>>;
|
||||||
|
|
||||||
|
export const data = async (pageContext: PageContextServer) => {
|
||||||
|
const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`);
|
||||||
|
let movie = (await response.json()) as MovieDetails;
|
||||||
|
// We remove data we don't need because the data is passed to
|
||||||
|
// the client; we should minimize what is sent over the network.
|
||||||
|
movie = minimize(movie);
|
||||||
|
return movie;
|
||||||
|
};
|
||||||
|
|
||||||
|
function minimize(movie: MovieDetails): MovieDetails {
|
||||||
|
const { id, title, release_date, director, producer } = movie;
|
||||||
|
const minimizedMovie = { id, title, release_date, director, producer };
|
||||||
|
return minimizedMovie;
|
||||||
|
}
|
7
pages/star-wars/@id/+title.ts
Normal file
7
pages/star-wars/@id/+title.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { PageContext } from "vike/types";
|
||||||
|
import type { Data } from "./+data.js";
|
||||||
|
|
||||||
|
export function title(pageContext: PageContext<Data>) {
|
||||||
|
const movie = pageContext.data;
|
||||||
|
return movie.title;
|
||||||
|
}
|
22
pages/star-wars/index/+Page.tsx
Normal file
22
pages/star-wars/index/+Page.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useData } from "vike-react/useData";
|
||||||
|
import type { Data } from "./+data.js";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const movies = useData<Data>();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Star Wars Movies</h1>
|
||||||
|
<ol>
|
||||||
|
{movies.map(({ id, title, release_date }) => (
|
||||||
|
<li key={id}>
|
||||||
|
<a href={`/star-wars/${id}`}>{title}</a> ({release_date})
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
22
pages/star-wars/index/+data.ts
Normal file
22
pages/star-wars/index/+data.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// https://vike.dev/data
|
||||||
|
|
||||||
|
import fetch from "cross-fetch";
|
||||||
|
import type { Movie, MovieDetails } from "../types.js";
|
||||||
|
|
||||||
|
export type Data = Awaited<ReturnType<typeof data>>;
|
||||||
|
|
||||||
|
export const data = async () => {
|
||||||
|
const response = await fetch("https://brillout.github.io/star-wars/api/films.json");
|
||||||
|
const moviesData = (await response.json()) as MovieDetails[];
|
||||||
|
// We remove data we don't need because the data is passed to the client; we should
|
||||||
|
// minimize what is sent over the network.
|
||||||
|
const movies = minimize(moviesData);
|
||||||
|
return movies;
|
||||||
|
};
|
||||||
|
|
||||||
|
function minimize(movies: MovieDetails[]): Movie[] {
|
||||||
|
return movies.map((movie) => {
|
||||||
|
const { title, release_date, id } = movie;
|
||||||
|
return { title, release_date, id };
|
||||||
|
});
|
||||||
|
}
|
7
pages/star-wars/index/+title.ts
Normal file
7
pages/star-wars/index/+title.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { PageContext } from "vike/types";
|
||||||
|
import type { Data } from "./+data.js";
|
||||||
|
|
||||||
|
export function title(pageContext: PageContext<Data>) {
|
||||||
|
const movies = pageContext.data;
|
||||||
|
return `${movies.length} Star Wars Movies`;
|
||||||
|
}
|
10
pages/star-wars/types.ts
Normal file
10
pages/star-wars/types.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export type Movie = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
release_date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MovieDetails = Movie & {
|
||||||
|
director: string;
|
||||||
|
producer: string;
|
||||||
|
};
|
27
pages/todo/+Page.tsx
Normal file
27
pages/todo/+Page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useData } from "vike-react/useData";
|
||||||
|
import type { Data } from "./+data.js";
|
||||||
|
import { TodoList } from "./TodoList.js";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const todoItemsInitial = useData<Data>();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>To-do List</h1>
|
||||||
|
<TodoList todoItemsInitial={todoItemsInitial} />
|
||||||
|
<Counter />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
This page is interactive:
|
||||||
|
<button type="button" onClick={() => setCount((count) => count + 1)}>
|
||||||
|
Counter {count}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
5
pages/todo/+config.ts
Normal file
5
pages/todo/+config.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const config = {
|
||||||
|
prerender: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
9
pages/todo/+data.ts
Normal file
9
pages/todo/+data.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// https://vike.dev/data
|
||||||
|
import { todoItems } from "../../database/todoItems";
|
||||||
|
|
||||||
|
export type Data = ReturnType<typeof data>;
|
||||||
|
|
||||||
|
export default function data() {
|
||||||
|
const todoItemsInitial = todoItems;
|
||||||
|
return todoItemsInitial;
|
||||||
|
}
|
6
pages/todo/TodoList.telefunc.ts
Normal file
6
pages/todo/TodoList.telefunc.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { todoItems, type TodoItem } from "../../database/todoItems";
|
||||||
|
|
||||||
|
export async function onNewTodo({ text }: TodoItem) {
|
||||||
|
todoItems.push({ text });
|
||||||
|
return { todoItems };
|
||||||
|
}
|
38
pages/todo/TodoList.tsx
Normal file
38
pages/todo/TodoList.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import type { TodoItem } from "../../database/todoItems";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { onNewTodo } from "./TodoList.telefunc.js";
|
||||||
|
|
||||||
|
export function TodoList({
|
||||||
|
todoItemsInitial,
|
||||||
|
}: {
|
||||||
|
todoItemsInitial: TodoItem[];
|
||||||
|
}) {
|
||||||
|
const [todoItems, setTodoItems] = useState(todoItemsInitial);
|
||||||
|
const [draft, setDraft] = useState("");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul>
|
||||||
|
{todoItems.map((todoItem, i) => (
|
||||||
|
<li key={i}>{todoItem.text}</li>
|
||||||
|
))}
|
||||||
|
<li>
|
||||||
|
<form
|
||||||
|
onSubmit={async (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const { todoItems } = await onNewTodo({ text: draft });
|
||||||
|
setDraft("");
|
||||||
|
setTodoItems(todoItems);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={(ev) => setDraft(ev.target.value)}
|
||||||
|
value={draft}
|
||||||
|
/>{" "}
|
||||||
|
<button type="submit">Add to-do</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
31
prisma/schema.prisma
Normal file
31
prisma/schema.prisma
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
// provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id
|
||||||
|
sessions Session[]
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
email String @unique
|
||||||
|
hashedPassword String
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id
|
||||||
|
expiresAt DateTime
|
||||||
|
userId String
|
||||||
|
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||||
|
}
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"noEmit": true,
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"types": ["vite/client", "vike-react"],
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "react",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
1
vike.d.ts
vendored
Normal file
1
vike.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
37
vite.config.ts
Normal file
37
vite.config.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import vercel from "vite-plugin-vercel";
|
||||||
|
import { telefunc } from "telefunc/vite";
|
||||||
|
import ssr from "vike/plugin";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import devServer from "@hono/vite-dev-server";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"~": __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
devServer({
|
||||||
|
entry: "hono-entry.ts",
|
||||||
|
|
||||||
|
exclude: [
|
||||||
|
/^\/@.+$/,
|
||||||
|
/.*\.(ts|tsx|vue)($|\?)/,
|
||||||
|
/.*\.(s?css|less)($|\?)/,
|
||||||
|
/^\/favicon\.ico$/,
|
||||||
|
/.*\.(svg|png)($|\?)/,
|
||||||
|
/^\/(public|assets|static)\/.+/,
|
||||||
|
/^\/node_modules\/.*/,
|
||||||
|
],
|
||||||
|
|
||||||
|
injectClientScript: false,
|
||||||
|
}),
|
||||||
|
react({}),
|
||||||
|
ssr({
|
||||||
|
prerender: true,
|
||||||
|
}),
|
||||||
|
telefunc(),
|
||||||
|
// vercel(),
|
||||||
|
],
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue