Merge pull request #886 from wasp-lang/vince-add-todo-ts-example

add todo app w/ typescript
This commit is contained in:
vincanger 2023-01-17 10:40:22 +01:00 committed by GitHub
commit 9ca12d4768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 520 additions and 0 deletions

3
examples/todo-typescript/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.wasp/
/.env.server
/.env.client

View File

@ -0,0 +1 @@
File marking the root of Wasp project.

View File

@ -0,0 +1,68 @@
app TodoTypescript {
wasp: {
version: "^0.7.3"
},
title: "ToDo TypeScript",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login",
}
}
// Use Prisma Schema Language (PSL) to define our entities: https://www.prisma.io/docs/concepts/components/prisma-schema
// Run `wasp db migrate-dev` in the CLI to create the database tables
// Then run `wasp db studio` to open Prisma Studio and view your db models
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[]
psl=}
entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User? @relation(fields: [userId], references: [id])
userId Int?
psl=}
route RootRoute { path: "/", to: MainPage }
page MainPage {
authRequired: true,
component: import { MainPage } from "@client/MainPage"
}
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@client/LoginPage"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@client/SignupPage"
}
query getTasks {
// We specify the JS implementation of our query (which is an async JS function)
// Even if you use TS and have a queries.ts file, you will still need to import it using the .js extension.
// see here for more info: https://wasp-lang.dev/docs/tutorials/todo-app/03-listing-tasks#wasp-declaration
fn: import { getTasks } from "@server/queries.js",
// We tell Wasp that this query is doing something with the `Task` entity. With that, Wasp will
// automatically refresh the results of this query when tasks change.
entities: [Task]
}
action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}
action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}

View File

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false,
"userId" INTEGER,
CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View File

@ -0,0 +1,3 @@
# Ignore editor tmp files
**/*~
**/#*#

View File

@ -0,0 +1,16 @@
import { Link } from 'react-router-dom';
import LoginForm from '@wasp/auth/forms/Login';
export function LoginPage() {
return (
<main>
<h1>Login</h1>
{/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */}
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to='/signup'>go to signup</Link>).
</span>
</main>
);
};

View File

@ -0,0 +1,53 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
main {
padding: 1rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
h1 {
padding: 0;
margin: 1rem 0;
}
main p {
font-size: 1rem;
}
img {
max-height: 100px;
}
button {
margin-top: 1rem;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.auth-form h2 {
margin-top: 0.5rem;
font-size: 1.2rem;
}
.tasklist {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 300px;
margin-top: 1rem;
}

View File

@ -0,0 +1,88 @@
import './Main.css';
import React, { useEffect, FormEventHandler, FormEvent } from 'react';
import logout from '@wasp/auth/logout.js';
import useAuth from '@wasp/auth/useAuth.js';
import { useQuery } from '@wasp/queries'; // Wasp uses a thin wrapper around react-query
import getTasks from '@wasp/queries/getTasks';
import createTask from '@wasp/actions/createTask';
import updateTask from '@wasp/actions/updateTask';
import waspLogo from './waspLogo.png';
import { Task } from './types'
export function MainPage() {
const { data: user } = useAuth();
const { data: tasks, isLoading, error } = useQuery<unknown, Task[]>(getTasks);
useEffect(() => {
console.log(user);
}, [user]);
if (isLoading) return 'Loading...';
if (error) return 'Error: ' + error;
return (
<main>
<img src={waspLogo} alt='wasp logo' />
<h1>
{user.username}
{`'s tasks :)`}
</h1>
<NewTaskForm />
{tasks && <TasksList tasks={tasks} /> }
<button onClick={logout}> Logout </button>
</main>
);
};
function Todo({ id, isDone, description }: Task) {
const handleIsDoneChange = async (event: FormEventHandler<HTMLInputElement>) => {
try {
await updateTask({
taskId: id,
isDone: event.currentTarget.checked,
});
} catch (err: any) {
window.alert('Error while updating task ' + err?.message);
}
};
return (
<li>
<input type='checkbox' id={id.toString()} checked={isDone} onChange={handleIsDoneChange} />
<span>{description}</span>
</li>
);
};
function TasksList({tasks}: { tasks: Task[] }) {
if (tasks.length === 0) return <p>No tasks yet.</p>;
return (
<ol className='tasklist'>
{tasks.map((tsk, idx) => (
<Todo {...tsk} key={idx} />
))}
</ol>
);
};
function NewTaskForm() {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
const description = event.currentTarget.description.value;
console.log(description)
event.currentTarget.reset();
await createTask({ description });
} catch (err: any) {
window.alert('Error: ' + err?.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input name='description' type='text' defaultValue='' />
<input type='submit' value='Create task' />
</form>
);
};

View File

@ -0,0 +1,16 @@
import { Link } from 'react-router-dom';
import SignupForm from '@wasp/auth/forms/Signup';
export function SignupPage() {
return (
<main>
<h1>Sign Up</h1>
{/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */}
<SignupForm />
<br />
<span>
I already have an account (<Link to='/login'>go to login</Link>).
</span>
</main>
);
};

View File

@ -0,0 +1,60 @@
declare module '*.avif' {
const src: string;
export default src;
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<
SVGSVGElement
> & { title?: string }>;
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
}

View File

@ -0,0 +1,55 @@
// =============================== IMPORTANT =================================
//
// This file is only used for Wasp IDE support. You can change it to configure
// your IDE checks, but none of these options will affect the TypeScript
// compiler. Proper TS compiler configuration in Wasp is coming soon :)
{
"compilerOptions": {
// JSX support
"jsx": "preserve",
"strict": true,
// Allow default imports.
"esModuleInterop": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
// Wasp needs the following settings enable IDE support in your source
// files. Editing them might break features like import autocompletion and
// definition lookup. Don't change them unless you know what you're doing.
//
// The relative path to the generated web app's root directory. This must be
// set to define the "paths" option.
"baseUrl": "../../.wasp/out/web-app/",
"paths": {
// Resolve all "@wasp" imports to the generated source code.
"@wasp/*": [
"src/*"
],
// Resolve all non-relative imports to the correct node module. Source:
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
"*": [
// Start by looking for the definiton inside the node modules root
// directory...
"node_modules/*",
// ... If that fails, try to find it inside definitely-typed type
// definitions.
"node_modules/@types/*"
]
},
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
"typeRoots": [
"../../.wasp/out/web-app/node_modules/@types"
],
// Since this TS config is used only for IDE support and not for
// compilation, the following directory doesn't exist. We need to specify
// it to prevent this error:
// https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file
"outDir": "phantom"
},
"exclude": [
"phantom"
],
}

View File

@ -0,0 +1,6 @@
export type Task = {
id: number;
description: string;
isDone: boolean;
userId: number | null;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,34 @@
import HttpError from '@wasp/core/HttpError.js';
import { Context, Task } from './serverTypes'
type CreateArgs = Pick<Task, 'description'>;
export async function createTask({ description }: CreateArgs, context: Context) {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.create({
data: {
description,
user: { connect: { id: context.user.id } },
},
});
};
// type UpdateArgs = { taskId: Task['id']; isDone: Task['isDone'] };
type UpdateArgs = Pick<Task, 'id' | 'isDone'>;
export async function updateTask({ id, isDone }: UpdateArgs, context: Context) {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.updateMany({
where: {
id,
user: { id: context.user.id },
},
data: { isDone },
});
};

View File

@ -0,0 +1,9 @@
import HttpError from '@wasp/core/HttpError.js';
import { Context, Task } from './serverTypes'
export async function getTasks(args: unknown, context: Context): Promise<Task[]> {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.findMany({ where: { user: { id: context.user.id } } });
};

View File

@ -0,0 +1,11 @@
import { User, Prisma } from '@prisma/client';
export { Task } from '@prisma/client';
export type Context = {
user: User;
entities: {
Task: Prisma.TaskDelegate<{}>;
User: Prisma.UserDelegate<{}>;
};
};

View File

@ -0,0 +1,48 @@
// =============================== IMPORTANT =================================
//
// This file is only used for Wasp IDE support. You can change it to configure
// your IDE checks, but none of these options will affect the TypeScript
// compiler. Proper TS compiler configuration in Wasp is coming soon :)
{
"compilerOptions": {
// Allows default imports.
"esModuleInterop": true,
"allowJs": true,
"strict": true,
// Wasp needs the following settings enable IDE support in your source
// files. Editing them might break features like import autocompletion and
// definition lookup. Don't change them unless you know what you're doing.
//
// The relative path to the generated web app's root directory. This must be
// set to define the "paths" option.
"baseUrl": "../../.wasp/out/server/",
"paths": {
// Resolve all "@wasp" imports to the generated source code.
"@wasp/*": [
"src/*"
],
// Resolve all non-relative imports to the correct node module. Source:
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
"*": [
// Start by looking for the definiton inside the node modules root
// directory...
"node_modules/*",
// ... If that fails, try to find it inside definitely-typed type
// definitions.
"node_modules/@types/*"
]
},
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
"typeRoots": [
"../../.wasp/out/server/node_modules/@types"
],
// Since this TS config is used only for IDE support and not for
// compilation, the following directory doesn't exist. We need to specify
// it to prevent this error:
// https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file
"outDir": "phantom",
},
"exclude": [
"phantom"
],
}

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
// Enable default imports in TypeScript.
"esModuleInterop": true,
"allowJs": true,
// The following settings enable IDE support in user-provided source files.
// Editing them might break features like import autocompletion and
// definition lookup. Don't change them unless you know what you're doing.
//
// The relative path to the generated web app's root directory. This must be
// set to define the "paths" option.
"baseUrl": "../../.wasp/out/server/",
"paths": {
// Resolve all non-relative imports to the correct node module. Source:
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
"*": [
// Start by looking for the definiton inside the node modules root
// directory...
"node_modules/*",
// ... If that fails, try to find it inside definitely-typed type
// definitions.
"node_modules/@types/*"
]
},
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
"typeRoots": ["../../.wasp/out/server/node_modules/@types"],
}
}