From b344a74a7245bd697eef43f291e98db8921405af Mon Sep 17 00:00:00 2001 From: Sebastian Korotkiewicz Date: Tue, 28 May 2024 08:08:42 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + README.md | 88 +++++++++++++++++++ assets/logo.svg | 36 ++++++++ bun.lockb | Bin 0 -> 154868 bytes components/Input.tsx | 15 ++++ components/Link.tsx | 13 +++ database/todoItems.ts | 12 +++ hono-entry.ts | 80 +++++++++++++++++ layouts/HeadDefault.tsx | 14 +++ layouts/LayoutDefault.tsx | 88 +++++++++++++++++++ layouts/style.css | 29 +++++++ lib/auth.ts | 148 ++++++++++++++++++++++++++++++++ lib/lucia.ts | 34 ++++++++ lib/prisma.ts | 5 ++ package.json | 43 ++++++++++ pages/+config.ts | 14 +++ pages/+onPageTransitionEnd.ts | 6 ++ pages/+onPageTransitionStart.ts | 6 ++ pages/PageContext.ts | 12 +++ pages/_error/+Page.tsx | 20 +++++ pages/account/+Page.tsx | 31 +++++++ pages/account/+guard.ts | 12 +++ pages/account/User.telefunc.ts | 16 ++++ pages/account/User.tsx | 20 +++++ pages/auth/+Page.tsx | 16 ++++ pages/auth/sign-in/+Page.tsx | 66 ++++++++++++++ pages/auth/sign-up/+Page.tsx | 92 ++++++++++++++++++++ pages/index/+Page.tsx | 17 ++++ pages/index/Counter.tsx | 11 +++ pages/star-wars/@id/+Page.tsx | 17 ++++ pages/star-wars/@id/+data.ts | 22 +++++ pages/star-wars/@id/+title.ts | 7 ++ pages/star-wars/index/+Page.tsx | 22 +++++ pages/star-wars/index/+data.ts | 22 +++++ pages/star-wars/index/+title.ts | 7 ++ pages/star-wars/types.ts | 10 +++ pages/todo/+Page.tsx | 27 ++++++ pages/todo/+config.ts | 5 ++ pages/todo/+data.ts | 9 ++ pages/todo/TodoList.telefunc.ts | 6 ++ pages/todo/TodoList.tsx | 38 ++++++++ prisma/schema.prisma | 31 +++++++ tsconfig.json | 24 ++++++ vike.d.ts | 1 + vite.config.ts | 37 ++++++++ 45 files changed, 1232 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 assets/logo.svg create mode 100755 bun.lockb create mode 100644 components/Input.tsx create mode 100644 components/Link.tsx create mode 100644 database/todoItems.ts create mode 100644 hono-entry.ts create mode 100644 layouts/HeadDefault.tsx create mode 100644 layouts/LayoutDefault.tsx create mode 100644 layouts/style.css create mode 100644 lib/auth.ts create mode 100644 lib/lucia.ts create mode 100644 lib/prisma.ts create mode 100644 package.json create mode 100644 pages/+config.ts create mode 100644 pages/+onPageTransitionEnd.ts create mode 100644 pages/+onPageTransitionStart.ts create mode 100644 pages/PageContext.ts create mode 100644 pages/_error/+Page.tsx create mode 100644 pages/account/+Page.tsx create mode 100644 pages/account/+guard.ts create mode 100644 pages/account/User.telefunc.ts create mode 100644 pages/account/User.tsx create mode 100644 pages/auth/+Page.tsx create mode 100644 pages/auth/sign-in/+Page.tsx create mode 100644 pages/auth/sign-up/+Page.tsx create mode 100644 pages/index/+Page.tsx create mode 100644 pages/index/Counter.tsx create mode 100644 pages/star-wars/@id/+Page.tsx create mode 100644 pages/star-wars/@id/+data.ts create mode 100644 pages/star-wars/@id/+title.ts create mode 100644 pages/star-wars/index/+Page.tsx create mode 100644 pages/star-wars/index/+data.ts create mode 100644 pages/star-wars/index/+title.ts create mode 100644 pages/star-wars/types.ts create mode 100644 pages/todo/+Page.tsx create mode 100644 pages/todo/+config.ts create mode 100644 pages/todo/+data.ts create mode 100644 pages/todo/TodoList.telefunc.ts create mode 100644 pages/todo/TodoList.tsx create mode 100644 prisma/schema.prisma create mode 100644 tsconfig.json create mode 100644 vike.d.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..842c9bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dev.db +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..145b5c4 --- /dev/null +++ b/README.md @@ -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 [`` component](https://vike.dev/Layout) (that wraps your [`` components](https://vike.dev/Page)). +- A default [`title`](https://vike.dev/head). +- Default [`` 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. diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..94d3caa --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,36 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..82888f1ce816a3061f6f67d1836c23d438a4fe5e GIT binary patch literal 154868 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 literal 0 HcmV?d00001 diff --git a/components/Input.tsx b/components/Input.tsx new file mode 100644 index 0000000..fa03c3c --- /dev/null +++ b/components/Input.tsx @@ -0,0 +1,15 @@ +import React, { InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + id: string; +} + +export function Input({ id, ...props }: InputProps) { + return ( + + ); +} diff --git a/components/Link.tsx b/components/Link.tsx new file mode 100644 index 0000000..8ae6ae4 --- /dev/null +++ b/components/Link.tsx @@ -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 ( + + {children} + + ); +} diff --git a/database/todoItems.ts b/database/todoItems.ts new file mode 100644 index 0000000..46d4e8b --- /dev/null +++ b/database/todoItems.ts @@ -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 }; diff --git a/hono-entry.ts b/hono-entry.ts new file mode 100644 index 0000000..ef1b759 --- /dev/null +++ b/hono-entry.ts @@ -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; diff --git a/layouts/HeadDefault.tsx b/layouts/HeadDefault.tsx new file mode 100644 index 0000000..cfdcd41 --- /dev/null +++ b/layouts/HeadDefault.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import logoUrl from "../assets/logo.svg"; + +// Default (can be overridden by pages) + +export default function HeadDefault() { + return ( + <> + + + + + ); +} diff --git a/layouts/LayoutDefault.tsx b/layouts/LayoutDefault.tsx new file mode 100644 index 0000000..04c6f89 --- /dev/null +++ b/layouts/LayoutDefault.tsx @@ -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 ( +
+ + + Welcome + Todo (telefunc) + Data Fetching + + {pageContext.auth ? ( + Account + ) : ( + Auth + )} + + {children} +
+ ); +} + +function Sidebar({ children }: { children: React.ReactNode }) { + return ( + + ); +} + +function Content({ children }: { children: React.ReactNode }) { + return ( +
+
+ {children} +
+
+ ); +} + +function Logo() { + return ( +
+ + logo + +
+ ); +} diff --git a/layouts/style.css b/layouts/style.css new file mode 100644 index 0000000..c5a3d28 --- /dev/null +++ b/layouts/style.css @@ -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; +} diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..c1fb484 --- /dev/null +++ b/lib/auth.ts @@ -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 + ); +}; diff --git a/lib/lucia.ts b/lib/lucia.ts new file mode 100644 index 0000000..68f9f00 --- /dev/null +++ b/lib/lucia.ts @@ -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; +} diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..1299460 --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,5 @@ +// import { PrismaClient } from '@prisma/client/edge' +import { PrismaClient } from "@prisma/client"; +const prisma = new PrismaClient(); + +export { prisma }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6992751 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/pages/+config.ts b/pages/+config.ts new file mode 100644 index 0000000..11f7009 --- /dev/null +++ b/pages/+config.ts @@ -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: "My Vike App", + extends: vikeReact, + passToClient: ["auth"], +} satisfies Config; diff --git a/pages/+onPageTransitionEnd.ts b/pages/+onPageTransitionEnd.ts new file mode 100644 index 0000000..75af2e0 --- /dev/null +++ b/pages/+onPageTransitionEnd.ts @@ -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"); +}; diff --git a/pages/+onPageTransitionStart.ts b/pages/+onPageTransitionStart.ts new file mode 100644 index 0000000..12c344b --- /dev/null +++ b/pages/+onPageTransitionStart.ts @@ -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"); +}; diff --git a/pages/PageContext.ts b/pages/PageContext.ts new file mode 100644 index 0000000..d542df6 --- /dev/null +++ b/pages/PageContext.ts @@ -0,0 +1,12 @@ +import type { User } from "lucia"; + +// https://vike.dev/pageContext#typescript +declare global { + namespace Vike { + interface PageContext { + auth: User; + } + } +} + +export {}; diff --git a/pages/_error/+Page.tsx b/pages/_error/+Page.tsx new file mode 100644 index 0000000..cd01d65 --- /dev/null +++ b/pages/_error/+Page.tsx @@ -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> + </> + ); +} diff --git a/pages/account/+Page.tsx b/pages/account/+Page.tsx new file mode 100644 index 0000000..5b8c222 --- /dev/null +++ b/pages/account/+Page.tsx @@ -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} /> + </> + ); +} diff --git a/pages/account/+guard.ts b/pages/account/+guard.ts new file mode 100644 index 0000000..c136d60 --- /dev/null +++ b/pages/account/+guard.ts @@ -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"); + } +}; diff --git a/pages/account/User.telefunc.ts b/pages/account/User.telefunc.ts new file mode 100644 index 0000000..4205e07 --- /dev/null +++ b/pages/account/User.telefunc.ts @@ -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) {} +}; diff --git a/pages/account/User.tsx b/pages/account/User.tsx new file mode 100644 index 0000000..5fc974b --- /dev/null +++ b/pages/account/User.tsx @@ -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; diff --git a/pages/auth/+Page.tsx b/pages/auth/+Page.tsx new file mode 100644 index 0000000..970a746 --- /dev/null +++ b/pages/auth/+Page.tsx @@ -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; diff --git a/pages/auth/sign-in/+Page.tsx b/pages/auth/sign-in/+Page.tsx new file mode 100644 index 0000000..e4ce3ac --- /dev/null +++ b/pages/auth/sign-in/+Page.tsx @@ -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; diff --git a/pages/auth/sign-up/+Page.tsx b/pages/auth/sign-up/+Page.tsx new file mode 100644 index 0000000..6c14863 --- /dev/null +++ b/pages/auth/sign-up/+Page.tsx @@ -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; diff --git a/pages/index/+Page.tsx b/pages/index/+Page.tsx new file mode 100644 index 0000000..0955fda --- /dev/null +++ b/pages/index/+Page.tsx @@ -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> + </> + ); +} diff --git a/pages/index/Counter.tsx b/pages/index/Counter.tsx new file mode 100644 index 0000000..00ea9aa --- /dev/null +++ b/pages/index/Counter.tsx @@ -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> + ); +} diff --git a/pages/star-wars/@id/+Page.tsx b/pages/star-wars/@id/+Page.tsx new file mode 100644 index 0000000..6e50b0e --- /dev/null +++ b/pages/star-wars/@id/+Page.tsx @@ -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} + </> + ); +} diff --git a/pages/star-wars/@id/+data.ts b/pages/star-wars/@id/+data.ts new file mode 100644 index 0000000..1ca9d2c --- /dev/null +++ b/pages/star-wars/@id/+data.ts @@ -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; +} diff --git a/pages/star-wars/@id/+title.ts b/pages/star-wars/@id/+title.ts new file mode 100644 index 0000000..7e9de82 --- /dev/null +++ b/pages/star-wars/@id/+title.ts @@ -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; +} diff --git a/pages/star-wars/index/+Page.tsx b/pages/star-wars/index/+Page.tsx new file mode 100644 index 0000000..3c24a4e --- /dev/null +++ b/pages/star-wars/index/+Page.tsx @@ -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> + </> + ); +} diff --git a/pages/star-wars/index/+data.ts b/pages/star-wars/index/+data.ts new file mode 100644 index 0000000..9385c2a --- /dev/null +++ b/pages/star-wars/index/+data.ts @@ -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 }; + }); +} diff --git a/pages/star-wars/index/+title.ts b/pages/star-wars/index/+title.ts new file mode 100644 index 0000000..fc6e4e1 --- /dev/null +++ b/pages/star-wars/index/+title.ts @@ -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`; +} diff --git a/pages/star-wars/types.ts b/pages/star-wars/types.ts new file mode 100644 index 0000000..ffccdf5 --- /dev/null +++ b/pages/star-wars/types.ts @@ -0,0 +1,10 @@ +export type Movie = { + id: string; + title: string; + release_date: string; +}; + +export type MovieDetails = Movie & { + director: string; + producer: string; +}; diff --git a/pages/todo/+Page.tsx b/pages/todo/+Page.tsx new file mode 100644 index 0000000..09b35ac --- /dev/null +++ b/pages/todo/+Page.tsx @@ -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> + ); +} diff --git a/pages/todo/+config.ts b/pages/todo/+config.ts new file mode 100644 index 0000000..e7f60cd --- /dev/null +++ b/pages/todo/+config.ts @@ -0,0 +1,5 @@ +const config = { + prerender: false, +}; + +export default config; diff --git a/pages/todo/+data.ts b/pages/todo/+data.ts new file mode 100644 index 0000000..2a13056 --- /dev/null +++ b/pages/todo/+data.ts @@ -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; +} diff --git a/pages/todo/TodoList.telefunc.ts b/pages/todo/TodoList.telefunc.ts new file mode 100644 index 0000000..e07340b --- /dev/null +++ b/pages/todo/TodoList.telefunc.ts @@ -0,0 +1,6 @@ +import { todoItems, type TodoItem } from "../../database/todoItems"; + +export async function onNewTodo({ text }: TodoItem) { + todoItems.push({ text }); + return { todoItems }; +} diff --git a/pages/todo/TodoList.tsx b/pages/todo/TodoList.tsx new file mode 100644 index 0000000..b74aad2 --- /dev/null +++ b/pages/todo/TodoList.tsx @@ -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> + </> + ); +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..04741bd --- /dev/null +++ b/prisma/schema.prisma @@ -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) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d41744 --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/vike.d.ts b/vike.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/vike.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..32182a1 --- /dev/null +++ b/vite.config.ts @@ -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(), + ], +});