mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-24 17:44:21 +03:00
Merge pull request #886 from wasp-lang/vince-add-todo-ts-example
add todo app w/ typescript
This commit is contained in:
commit
9ca12d4768
3
examples/todo-typescript/.gitignore
vendored
Normal file
3
examples/todo-typescript/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/.wasp/
|
||||
/.env.server
|
||||
/.env.client
|
1
examples/todo-typescript/.wasproot
Normal file
1
examples/todo-typescript/.wasproot
Normal file
@ -0,0 +1 @@
|
||||
File marking the root of Wasp project.
|
68
examples/todo-typescript/main.wasp
Normal file
68
examples/todo-typescript/main.wasp
Normal 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]
|
||||
}
|
@ -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");
|
3
examples/todo-typescript/migrations/migration_lock.toml
Normal file
3
examples/todo-typescript/migrations/migration_lock.toml
Normal 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"
|
3
examples/todo-typescript/src/.waspignore
Normal file
3
examples/todo-typescript/src/.waspignore
Normal file
@ -0,0 +1,3 @@
|
||||
# Ignore editor tmp files
|
||||
**/*~
|
||||
**/#*#
|
16
examples/todo-typescript/src/client/LoginPage.tsx
Normal file
16
examples/todo-typescript/src/client/LoginPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
53
examples/todo-typescript/src/client/Main.css
Normal file
53
examples/todo-typescript/src/client/Main.css
Normal 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;
|
||||
}
|
88
examples/todo-typescript/src/client/MainPage.tsx
Normal file
88
examples/todo-typescript/src/client/MainPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
16
examples/todo-typescript/src/client/SignupPage.tsx
Normal file
16
examples/todo-typescript/src/client/SignupPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
60
examples/todo-typescript/src/client/react-app-env.d.ts
vendored
Normal file
60
examples/todo-typescript/src/client/react-app-env.d.ts
vendored
Normal 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;
|
||||
}
|
55
examples/todo-typescript/src/client/tsconfig.json
Normal file
55
examples/todo-typescript/src/client/tsconfig.json
Normal 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"
|
||||
],
|
||||
}
|
6
examples/todo-typescript/src/client/types.ts
Normal file
6
examples/todo-typescript/src/client/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type Task = {
|
||||
id: number;
|
||||
description: string;
|
||||
isDone: boolean;
|
||||
userId: number | null;
|
||||
};
|
BIN
examples/todo-typescript/src/client/waspLogo.png
Normal file
BIN
examples/todo-typescript/src/client/waspLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
34
examples/todo-typescript/src/server/actions.ts
Normal file
34
examples/todo-typescript/src/server/actions.ts
Normal 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 },
|
||||
});
|
||||
};
|
9
examples/todo-typescript/src/server/queries.ts
Normal file
9
examples/todo-typescript/src/server/queries.ts
Normal 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 } } });
|
||||
};
|
11
examples/todo-typescript/src/server/serverTypes.ts
Normal file
11
examples/todo-typescript/src/server/serverTypes.ts
Normal 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<{}>;
|
||||
};
|
||||
};
|
48
examples/todo-typescript/src/server/tsconfig.json
Normal file
48
examples/todo-typescript/src/server/tsconfig.json
Normal 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"
|
||||
],
|
||||
}
|
28
examples/todo-typescript/src/shared/tsconfig.json
Normal file
28
examples/todo-typescript/src/shared/tsconfig.json
Normal 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"],
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user