Merge branch 'main' into wasp-ai

This commit is contained in:
Martin Sosic 2023-12-11 20:19:46 +01:00
commit 47c4b8cb16
436 changed files with 16365 additions and 13141 deletions

2
.gitattributes vendored
View File

@ -1,3 +1,5 @@
*.wasp linguist-language=JavaScript
*.hs linguist-detectable=false
/examples/* linguist-documentation

View File

@ -1,11 +1,15 @@
name: CI
name: WASPC-CI
on:
push:
paths:
- "waspc/**"
branches:
- main
- release
pull_request: { }
pull_request:
paths:
- "waspc/**"
create: { tags: [v*] }
schedule:
# Additionally run once per week (At 00:00 on Sunday) to avoid loosing cache

View File

@ -19,13 +19,13 @@
<br>
Wasp (**W**eb **A**pplication **Sp**ecification) is a Rails-like framework for React, Node.js and Prisma.
Wasp (**W**eb **A**pplication **Sp**ecification) is a Rails-like framework for React, Node.js, and Prisma.
Build your app in a day and deploy it with a single CLI command!
### Why is Wasp awesome
- 🚀 **Quick start**: Due to its expressiveness, you can create and deploy a production-ready web app from scratch with very few lines of concise, consistent, declarative code.
- 😌 **No boilerplate**: By abstracting away complex full-stack features, there is less boilerplate code. That means less code to maintain and understand! It also means easier upgrades.
- 🔓 **No lock-in**: You can deploy Wasp app anywhere you like. There is no lock-in into specific providers, you have full control over the code (and can actually check it out in .wasp/ dir if you are interested ).
- 🔓 **No lock-in**: You can deploy the Wasp app anywhere you like. There is no lock-in into specific providers; you have full control over the code (and can actually check it out in .wasp/ dir if you are interested ).
### Features
🔒 Full-stack Auth, 🖇️ RPC (Client <-> Server), 🚀 Simple Deployment, ⚙ Jobs, ✉️ Email Sending, 🛟 Full-stack Type Safety, ...
@ -46,7 +46,7 @@ app todoApp {
route RootRoute { path: "/", to: MainPage }
page MainPage {
authRequired: true, // Limit access to logged in users.
authRequired: true, // Limit access to logged-in users.
component: import Main from "@client/Main.tsx" // Your React code.
}
@ -72,7 +72,7 @@ The rest of the code you write in React / Node.js / Prisma and just reference it
Given a simple .wasp configuration file that describes the high-level details of your web app, and .js(x)/.css/..., source files with your unique logic, Wasp compiler generates the full source of your web app in the target stack: front-end, back-end and deployment.
This unique approach is what makes Wasp "smart" and gives it its super powers!
This unique approach is what makes Wasp "smart" and gives it its superpowers!
For more information about Wasp, check [**docs**](https://wasp-lang.dev/docs).
@ -84,7 +84,7 @@ curl -sSL https://get.wasp-lang.dev/installer.sh | sh
```
to install Wasp on OSX/Linux/WSL(Win). From there, just follow the instructions to run your first app in less than a minute!
For more details check out [the docs](https://wasp-lang.dev/docs).
For more details, check out [the docs](https://wasp-lang.dev/docs).
# This repository
@ -95,15 +95,15 @@ This is the main repo of the Wasp universe, containing core code (mostly `waspc`
Currently, Wasp is in beta, with most features flushed out and working well.
However, there are still a lot of improvements and additions that we have in mind for the future, and we are working on them constantly, so you can expect a lot of changes and improvements in the future.
While the idea is to support multiple web tech stacks in the future, right now we are focusing on the specific stack: React + react-query, NodeJS + ExpressJS, Prisma.
While the idea is to support multiple web tech stacks in the future, right now, we are focusing on the specific stack: React + react-query, NodeJS + ExpressJS, and Prisma.
# Contributing
Any way you want to contribute is a good way :)!
The best place to start is to check out [waspc/](waspc/), where you can find detailed steps for the first time contributors + technical details about the Wasp compiler.
The best place to start is to check out [waspc/](waspc/), where you can find detailed steps for first-time contributors + technical details about the Wasp compiler.
Core of Wasp is built in Haskell, but there is also a lot of non-Haskell parts of Wasp, so you will certainly be able to find something for you!
The core of Wasp is built in Haskell, but there are also a lot of non-Haskell parts of Wasp, so you will certainly be able to find something for you!
Even if you don't plan to submit any code, just joining the discussion on discord [![Discord](https://img.shields.io/discord/686873244791210014?label=chat%20on%20discord)](https://discord.gg/rzdnErX) and giving your feedback is already great and helps a lot (motivates us and helps us figure out how to shape Wasp)!
@ -118,4 +118,12 @@ Check our [careers](https://wasp-lang.notion.site/Wasp-Careers-59fd1682c80d446f9
# Sponsors
<a href="https://github.com/michelwaechter"><img src="https://github.com/michelwaechter.png" width="50px" alt="michelwaechter" /></a> - Our first sponsor ever! Thanks so much Michel ❤️ , from the whole Wasp Team, for bravely going where nobody has been before :)!
<a href="https://github.com/michelwaechter"><img src="https://github.com/michelwaechter.png" width="50px" alt="michelwaechter" /></a> - Our first sponsor ever! Thanks so much, Michel ❤️ , from the whole Wasp Team, for bravely going where nobody has been before :)!
<a href="https://github.com/shayneczyzewski"><img src="https://github.com/shayneczyzewski.png" width="50px" alt="shayneczyzewski" /></a> - Thanks Shayne, for all the contributions you did so far and for your continuous support!
<a href="https://github.com/MarianoMiguel"><img src="https://github.com/MarianoMiguel.png" width="50px" alt="MarianoMiguel" /></a> - Big thanks Mariano for being one of our first sponsors and believing in us ❤️!
<a href="https://github.com/Tech4Money"><img src="https://github.com/Tech4Money.png" width="50px" alt="Tech4Money" /></a> - Thanks Mike ❤️ for supporting us so early on our journey!
<a href="https://github.com/haseeb-heaven"><img src="https://github.com/haseeb-heaven.png" width="50px" alt="haseeb-heaven" /></a> - We are thankful for your support Heaven in this early stage of Wasp :)!

37
examples/README.md Normal file
View File

@ -0,0 +1,37 @@
Example Wasp apps
=================
Here's a list of all officially maintained Wasp example apps!
Most of these apps are relatively small, and each one demonstrates several of Wasp's interesting features.
The **tutorials** directory contains [Wasp tutorial](https://wasp-lang.dev/docs/tutorial/create) apps.
1. **todo-typescript**
- A simple task-tracking app implemented in TypeScript.
- Demonstrates: **full-stack type safety in Wasp via TypeScript**, [auth](https://wasp-lang.dev/docs/auth/overview), [rpc](https://wasp-lang.dev/docs/data-model/operations/overview)
2. **waspello**
- A simple Trello clone.
- Demonstrates: [auth](https://wasp-lang.dev/docs/auth/overview), [rpc](https://wasp-lang.dev/docs/data-model/operations/overview)
3. **hackaton-submissions**
- An app for organizing your own hackaton.
- Demonstrates: [tailwind](https://wasp-lang.dev/docs/project/css-frameworks#tailwind), [rpc](https://wasp-lang.dev/docs/data-model/operations/overview)
4. **waspleau**
- A simple clone of Tableau. The app regularly pulls in external data and shows it on a nice dashboard.
- Demonstrates: **[jobs](https://wasp-lang.dev/docs/advanced/jobs)**, analytics
5. **thoughts**
- A simple note-taking app organized around the concept of hashtags.
- Demonstrates: [db seeding](https://wasp-lang.dev/docs/data-model/backends#seeding-the-database), [auth](https://wasp-lang.dev/docs/auth/overview), [rpc](https://wasp-lang.dev/docs/data-model/operations/overview)
6. **realworld**
- A Medium clone (https://github.com/gothinkster/realworld - the mother of all demo apps).
- Demonstrates: [auth](https://wasp-lang.dev/docs/auth/overview), [rpc](https://wasp-lang.dev/docs/data-model/operations/overview), [db seeding](https://wasp-lang.dev/docs/data-model/backends#seeding-the-database), Material UI
7. **streaming**
- Demonstrates: **http streaming**, [api](https://wasp-lang.dev/docs/advanced/apis)
8. **websockets-realtime-voting**
- Demonstrates: **[web sockets](https://wasp-lang.dev/docs/advanced/web-sockets)**, [auth](https://wasp-lang.dev/docs/auth/overview), voting system

View File

@ -12,7 +12,7 @@ app hackathonBetaSubmissions {
}
entity Submission {=psl
name String @id @unique
name String @id @unique
email String @unique
github String
description String

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

11
examples/streaming/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/.wasp/
# We ignore env files recognized and used by Wasp.
.env.server
.env.client
# To be extra safe, we by default ignore any files with `.env` extension in them.
# If this is too agressive for you, consider allowing specific files with `!` operator,
# or modify/delete these two lines.
*.env
*.env.*

View File

@ -0,0 +1,21 @@
app streaming {
wasp: {
version: "^0.11.5"
},
title: "streaming"
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@client/MainPage.jsx"
}
api streamingText {
httpRoute: (GET, "/api/streaming-test"),
fn: import { getText } from "@server/streaming.js",
}
apiNamespace defaultMiddleware {
path: "/api",
middlewareConfigFn: import { getMiddlewareConfig } from "@server/streaming.js",
}

View File

@ -0,0 +1,89 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
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;
}

View File

@ -0,0 +1,57 @@
import { useState, useEffect } from "react";
import config from "@wasp/config";
import "./Main.css";
const MainPage = () => {
const { response } = useTextStream("/api/streaming-test");
return (
<div className="container">
<main>
<h1>Streaming Demo</h1>
<p
style={{
maxWidth: "600px",
}}
>
{response}
</p>
</main>
</div>
);
};
export default MainPage;
function useTextStream(path) {
const [response, setResponse] = useState("");
useEffect(() => {
const controller = new AbortController();
fetchStream(
path,
(chunk) => {
setResponse((prev) => prev + chunk);
},
controller
);
return () => {
controller.abort();
};
}, []);
return {
response,
};
}
async function fetchStream(path, onData, controller) {
const response = await fetch(config.apiUrl + path, {
signal: controller.signal,
});
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
onData(value.toString());
}
}

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 @@
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,29 @@
import { StreamingText } from "@wasp/apis/types";
import { MiddlewareConfigFn } from "@wasp/middleware";
// Custom API endpoint that returns a streaming text.
export const getText: StreamingText = async (req, res, context) => {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Transfer-Encoding", "chunked");
let counter = 1;
res.write("Hm, let me see...\n");
while (counter <= 10) {
// Send a chunk of data.
if (counter === 10) {
res.write(`and finally about ${counter}.`);
} else {
res.write(`let's talk about number ${counter} and `);
}
counter++;
// Wait for 1 second.
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// End the response.
res.end();
};
// Returning the default config.
export const getMiddlewareConfig: MiddlewareConfigFn = (config) => {
return config;
};

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

@ -1,12 +1,15 @@
{
"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",
"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
@ -18,6 +21,8 @@
// definitions.
"node_modules/@types/*"
]
}
},
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
"typeRoots": ["../../.wasp/out/server/node_modules/@types"]
}
}

View File

@ -5,11 +5,12 @@ Thoughts
Run `wasp start` to start the app in development mode.
This app is deployed at https://wasp-thoughts.netlify.app/ .
This app is deployed at https://wasp-thoughts.netlify.app/ : client at Netlify, server and db at Heroku.
It is also deployed at https://wasp-thoughts-client.fly.dev/ : client, server and db on Fly.io .
## TODO
## How it felt so far to build this app in Wasp
## How it felt so far to build this app in Wasp (2022)
Here I write down how I felt while developing this app, so we can use this feedback in the future to improve Wasp. Subjective feedback is also written down.

View File

@ -0,0 +1,17 @@
# fly.toml app configuration file generated for wasp-thoughts-client on 2023-10-13T13:45:18+02:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "wasp-thoughts-client"
primary_region = "mia"
[build]
[http_service]
internal_port = 8043
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

@ -0,0 +1,17 @@
# fly.toml app configuration file generated for wasp-thoughts-server on 2023-10-13T13:45:12+02:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "wasp-thoughts-server"
primary_region = "mia"
[build]
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

@ -7,7 +7,10 @@ app TodoTypescript {
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
usernameAndPassword: {}, // this is a very naive implementation, use 'email' in production instead
//google: {}, //https://wasp-lang.dev/docs/integrations/google
//gitHub: {}, //https://wasp-lang.dev/docs/integrations/github
//email: {} //https://wasp-lang.dev/docs/guides/email-auth
},
onAuthFailedRedirectTo: "/login",
}
@ -24,11 +27,11 @@ entity User {=psl
psl=}
entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User? @relation(fields: [userId], references: [id])
userId Int?
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 }
@ -66,3 +69,8 @@ action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
action deleteTasks {
fn: import { deleteTasks } from "@server/actions.js",
entities: [Task],
}

View File

@ -10,8 +10,8 @@ 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
"userId" INTEGER NOT NULL,
CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex

View File

@ -1,16 +1,17 @@
import { Link } from 'react-router-dom';
import { LoginForm } from '@wasp/auth/forms/Login';
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 :) */}
{/** Wasp has built-in auth forms & flows, which you can customize or opt-out of, if you wish :)
* https://wasp-lang.dev/docs/guides/auth-ui
*/}
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to='/signup'>go to signup</Link>).
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</main>
);
};
}

View File

@ -26,7 +26,7 @@ img {
max-height: 100px;
}
button {
.logout {
margin-top: 1rem;
}
@ -43,6 +43,13 @@ code {
font-size: 1.2rem;
}
.buttons {
display: flex;
flex-direction: row;
width: 300px;
justify-content: space-between;
}
.tasklist {
display: flex;
flex-direction: column;
@ -50,4 +57,17 @@ code {
justify-content: center;
width: 300px;
margin-top: 1rem;
padding: 0
}
li {
width: 100%;
}
.todo-item {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}

View File

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

View File

@ -1,16 +1,17 @@
import { Link } from 'react-router-dom';
import { SignupForm } from '@wasp/auth/forms/Signup';
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 :) */}
{/** Wasp has built-in auth forms & flows, which you can customize or opt-out of, if you wish :)
* https://wasp-lang.dev/docs/guides/auth-ui
*/}
<SignupForm />
<br />
<span>
I already have an account (<Link to='/login'>go to login</Link>).
I already have an account (<Link to="/login">go to login</Link>).
</span>
</main>
);
};
}

View File

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

View File

@ -1,9 +1,13 @@
import HttpError from '@wasp/core/HttpError.js';
import { Context, Task } from './serverTypes'
import HttpError from "@wasp/core/HttpError.js";
import type { CreateTask, UpdateTask, DeleteTasks } from "@wasp/actions/types";
import type { Task } from "@wasp/entities";
type CreateArgs = Pick<Task, 'description'>;
type CreateArgs = Pick<Task, "description">;
export async function createTask({ description }: CreateArgs, context: Context) {
export const createTask: CreateTask<CreateArgs, Task> = async (
{ description },
context
) => {
if (!context.user) {
throw new HttpError(401);
}
@ -16,18 +20,33 @@ export async function createTask({ description }: CreateArgs, context: Context)
});
};
type UpdateArgs = Pick<Task, 'id' | 'isDone'>;
type UpdateArgs = Pick<Task, "id" | "isDone">;
export async function updateTask({ id, isDone }: UpdateArgs, context: Context) {
export const updateTask: UpdateTask<UpdateArgs> = async (
{ id, isDone },
context
) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.updateMany({
return context.entities.Task.update({
where: {
id,
user: { id: context.user.id },
},
data: { isDone },
});
};
export const deleteTasks: DeleteTasks<Task["id"][]> = async (
idsToDelete,
context
) => {
return context.entities.Task.deleteMany({
where: {
id: {
in: idsToDelete,
},
},
});
};

View File

@ -1,9 +1,15 @@
import HttpError from '@wasp/core/HttpError.js';
import { Context, Task } from './serverTypes'
import HttpError from "@wasp/core/HttpError.js";
import type { GetTasks } from "@wasp/queries/types";
import type { Task } from "@wasp/entities";
export async function getTasks(args: unknown, context: Context): Promise<Task[]> {
//Using TypeScript's new 'satisfies' keyword, it will infer the types of the arguments and return value
export const getTasks = ((_args, context) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.findMany({ where: { user: { id: context.user.id } } });
};
return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
orderBy: { id: "asc" },
});
}) satisfies GetTasks<void, Task[]>;

View File

@ -1,11 +0,0 @@
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

@ -1,13 +0,0 @@
# It Wasps On My Machine - an app to retrieve and store excuses for all your dev needs
![Sponge Bob picks an excuse for today's stand-up](ext/sponge-bob-excuses.jpg)
It Wasps On My Machine is a simple full stack web app, that pulls dev excuses from [
devexcuses-api Github project](https://github.com/michelegera/devexcuses-api) and enables you to save the ones you liked (and your boss doesn't).
## Installation
Clone the repo, open the terminal in the containing folder, and run:
wasp db migrate-dev
wasp start

View File

@ -1,42 +0,0 @@
app ItWaspsOnMyMachine {
wasp: {
version: "^0.11.0"
},
title: "It Wasps On My Machine",
head: [
"<script src='https://cdn.tailwindcss.com'></script>"
],
dependencies: [
("axios", "^0.27.2")
]
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@client/MainPage.jsx"
}
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}
query getExcuse {
fn: import { getExcuse } from "@client/queries.js",
entities: [Excuse]
}
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

View File

@ -1,8 +0,0 @@
-- CreateTable
CREATE TABLE "Excuse" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"text" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Excuse_text_key" ON "Excuse"("text");

View File

@ -1,2 +0,0 @@
-- DropIndex
DROP INDEX "Excuse_text_key";

View File

@ -1,56 +0,0 @@
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'
const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)
const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}
const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}
return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}
const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}
const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}
export default MainPage

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

View File

@ -1,5 +0,0 @@
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}

View File

@ -1,10 +0,0 @@
import axios from 'axios';
export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}
export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

View File

@ -1,25 +1,25 @@
app TodoApp {
wasp: {
version: "^0.11.0"
version: "^0.11.6" // Pins the version of Wasp to use.
},
title: "Todo app",
title: "Todo app", // Used as the browser tab title. Note that all strings in Wasp are double quoted!
auth: {
// Tells Wasp which entity to use for storing users.
userEntity: User,
methods: {
// Enable username and password auth.
usernameAndPassword: {}
},
// We'll see how this is used a bit later.
onAuthFailedRedirectTo: "/login"
},
dependencies: [
("react-clock", "3.0.0")
]
}
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
// We specify that the React implementation of the page is the default export
// of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
// Use `@client` to reference files inside the `src/client` folder.
authRequired: true,
component: import Main from "@client/MainPage.jsx"
}
@ -35,22 +35,26 @@ page LoginPage {
}
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
tasks Task[]
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])
user User? @relation(fields: [userId], references: [id])
userId Int?
psl=}
query getTasks {
// Specifies where the implementation for the query function is.
// Use `@server` to import files inside the `src/server` folder.
fn: import { getTasks } from "@server/queries.js",
// Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
// will automatically update the results of this query when tasks are modified.
entities: [Task]
}

View File

@ -1,19 +0,0 @@
import { useEffect, useState } from 'react'
import Clock from 'react-clock'
import 'react-clock/dist/Clock.css'
export default () => {
const [time, setTime] = useState(new Date())
useEffect(() => {
const interval = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(interval)
}, [])
return (
<div style={{ display: 'flex' }}>
<Clock value={time} />
<Clock value={new Date(time.getTime() + 60 * 60000)} />
</div>
)
}

View File

@ -1,19 +1,16 @@
import { Link } from 'react-router-dom'
import { LoginForm } from '@wasp/auth/forms/Login'
const LoginPage = () => {
return (
<>
<div style={{maxWidth: "400px", margin: "0 auto"}}>
<LoginForm/>
<br/>
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
</>
<div style={{maxWidth: "400px", margin: "0 auto"}}>
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
)
}
export default LoginPage
export default LoginPage

View File

@ -1,12 +1,11 @@
import { useQuery } from '@wasp/queries'
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
import { useQuery } from '@wasp/queries'
import updateTask from '@wasp/actions/updateTask'
import logout from '@wasp/auth/logout'
import Clocks from './Clocks'
const MainPage = () => {
const { data: tasks, isFetching, error } = useQuery(getTasks)
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
@ -14,22 +13,19 @@ const MainPage = () => {
{tasks && <TasksList tasks={tasks} />}
<div> <Clocks /> </div>
{isFetching && 'Fetching...'}
{isLoading && 'Loading...'}
{error && 'Error: ' + error}
<button onClick={logout}> Logout </button>
<button onClick={logout}>Logout</button>
</div>
)
}
const Task = (props) => {
const Task = ({ task }) => {
const handleIsDoneChange = async (event) => {
try {
await updateTask({
taskId: props.task.id,
data: { isDone: event.target.checked }
id: task.id,
isDone: event.target.checked,
})
} catch (error) {
window.alert('Error while updating task: ' + error.message)
@ -38,26 +34,35 @@ const Task = (props) => {
return (
<div>
<input
type='checkbox' id={props.task.id}
checked={props.task.isDone}
type="checkbox"
id={String(task.id)}
checked={task.isDone}
onChange={handleIsDoneChange}
/>
{props.task.description}
{task.description}
</div>
)
}
const TasksList = (props) => {
if (!props.tasks?.length) return 'No tasks'
return props.tasks.map((task, idx) => <Task task={task} key={idx} />)
const TasksList = ({ tasks }) => {
if (!tasks?.length) return <div>No tasks</div>
return (
<div>
{tasks.map((task, idx) => (
<Task task={task} key={idx} />
))}
</div>
)
}
const NewTaskForm = (props) => {
const NewTaskForm = () => {
const handleSubmit = async (event) => {
event.preventDefault()
try {
const description = event.target.description.value
event.target.reset()
const target = event.target
const description = target.description.value
target.reset()
await createTask({ description })
} catch (err) {
window.alert('Error: ' + err.message)
@ -66,15 +71,10 @@ const NewTaskForm = (props) => {
return (
<form onSubmit={handleSubmit}>
<input
name='description'
type='text'
defaultValue=''
/>
<input type='submit' value='Create task' />
<input name="description" type="text" defaultValue="" />
<input type="submit" value="Create task" />
</form>
)
}
export default MainPage

View File

@ -1,19 +1,16 @@
import { Link } from 'react-router-dom'
import { SignupForm } from '@wasp/auth/forms/Signup'
const SignupPage = () => {
return (
<>
<div style={{maxWidth: "400px", margin: "0 auto"}}>
<SignupForm/>
<br/>
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
</>
<div style={{maxWidth: "400px", margin: "0 auto"}}>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
)
}
export default SignupPage
export default SignupPage

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
export default defineConfig({
server: {
open: true,
},
})

View File

@ -1,19 +1,23 @@
import HttpError from '@wasp/core/HttpError.js'
export const createTask = async (args, context) => {
if (!context.user) { throw new HttpError(401) }
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.create({
data: {
description: args.description,
user: { connect: { id: context.user.id } }
}
user: { connect: { id: context.user.id } },
},
})
}
export const updateTask = async (args, context) => {
if (!context.user) { throw new HttpError(401) }
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.updateMany({
where: { id: args.taskId, user: { id: context.user.id } },
data: { isDone: args.data.isDone }
where: { id: args.id, user: { id: context.user.id } },
data: { isDone: args.isDone },
})
}

View File

@ -1,8 +1,11 @@
import HttpError from '@wasp/core/HttpError.js'
export const getTasks = async (args, context) => {
if (!context.user) { throw new HttpError(401) }
return context.entities.Task.findMany(
{ where: { user: { id: context.user.id } } }
)
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
orderBy: { id: 'asc' },
})
}

11
examples/tutorials/TodoAppTs/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/.wasp/
# We ignore env files recognized and used by Wasp.
.env.server
.env.client
# To be extra safe, we by default ignore any files with `.env` extension in them.
# If this is too agressive for you, consider allowing specific files with `!` operator,
# or modify/delete these two lines.
*.env
*.env.*

View File

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

View File

@ -0,0 +1,69 @@
app TodoApp {
wasp: {
version: "^0.11.6" // Pins the version of Wasp to use.
},
title: "Todo app", // Used as the browser tab title. Note that all strings in Wasp are double quoted!
auth: {
// Tells Wasp which entity to use for storing users.
userEntity: User,
methods: {
// Enable username and password auth.
usernameAndPassword: {}
},
// We'll see how this is used a bit later.
onAuthFailedRedirectTo: "/login"
}
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
// We specify that the React implementation of the page is the default export
// of `src/client/MainPage.tsx`. This statement uses standard JS import syntax.
// Use `@client` to reference files inside the `src/client` folder.
authRequired: true,
component: import Main from "@client/MainPage.tsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/SignupPage.tsx"
}
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/LoginPage.tsx"
}
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=}
query getTasks {
// Specifies where the implementation for the query function is.
// Use `@server` to import files inside the `src/server` folder.
fn: import { getTasks } from "@server/queries.js",
// Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
// will automatically update the results of this query when tasks are modified.
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,6 @@
-- CreateTable
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"description" TEXT NOT NULL,
"isDone" BOOLEAN NOT NULL DEFAULT false
);

View File

@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

View File

@ -0,0 +1,14 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_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
);
INSERT INTO "new_Task" ("description", "id", "isDone") SELECT "description", "id", "isDone" FROM "Task";
DROP TABLE "Task";
ALTER TABLE "new_Task" RENAME TO "Task";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

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'
const LoginPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
)
}
export default LoginPage

View File

@ -0,0 +1,83 @@
import { FormEvent, ChangeEvent } from 'react'
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
import updateTask from '@wasp/actions/updateTask'
import { useQuery } from '@wasp/queries'
import { Task } from '@wasp/entities'
import logout from '@wasp/auth/logout'
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
<NewTaskForm />
{tasks && <TasksList tasks={tasks} />}
{isLoading && 'Loading...'}
{error && 'Error: ' + error}
<button onClick={logout}>Logout</button>
</div>
)
}
const Task = ({ task }: { task: Task }) => {
const handleIsDoneChange = async (event: ChangeEvent<HTMLInputElement>) => {
try {
await updateTask({
id: task.id,
isDone: event.target.checked,
})
} catch (error: any) {
window.alert('Error while updating task: ' + error.message)
}
}
return (
<div>
<input
type="checkbox"
id={String(task.id)}
checked={task.isDone}
onChange={handleIsDoneChange}
/>
{task.description}
</div>
)
}
const TasksList = ({ tasks }: { tasks: Task[] }) => {
if (!tasks?.length) return <div>No tasks</div>
return (
<div>
{tasks.map((task, idx) => (
<Task task={task} key={idx} />
))}
</div>
)
}
const NewTaskForm = () => {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
try {
const target = event.target as HTMLFormElement
const description = target.description.value
target.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>
)
}
export default MainPage

View File

@ -0,0 +1,16 @@
import { Link } from 'react-router-dom'
import { SignupForm } from '@wasp/auth/forms/Signup'
const SignupPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
)
}
export default SignupPage

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 @@
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
export default defineConfig({
server: {
open: true,
},
})

View File

@ -0,0 +1,35 @@
import { Task } from '@wasp/entities'
import { CreateTask, UpdateTask } from '@wasp/actions/types'
import HttpError from '@wasp/core/HttpError.js'
type CreateTaskPayload = Pick<Task, 'description'>
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
args,
context
) => {
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.create({
data: {
description: args.description,
user: { connect: { id: context.user.id } },
},
})
}
type UpdateTaskPayload = Pick<Task, 'id' | 'isDone'>
export const updateTask: UpdateTask<
UpdateTaskPayload,
{ count: number }
> = async ({ id, isDone }, 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,13 @@
import { Task } from '@wasp/entities'
import { GetTasks } from '@wasp/queries/types'
import HttpError from '@wasp/core/HttpError.js'
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
if (!context.user) {
throw new HttpError(401)
}
return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
orderBy: { id: 'asc' },
})
}

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

@ -1,17 +1,16 @@
{
"compilerOptions": {
// The following settings enable IDE support in user-provided source files.
// 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/web-app/",
"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
"*": [
@ -22,6 +21,8 @@
// definitions.
"node_modules/@types/*"
]
}
},
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
"typeRoots": ["../../.wasp/out/server/node_modules/@types"]
}
}

View File

@ -5,7 +5,7 @@ It pulls in data via [Jobs](https://wasp-lang.dev/docs/language/features#jobs) a
This example project can serve as a good starting point for building your own dashboard with Wasp, that regularly pulls in external data by using Jobs Wasp feature.
The deployed version of this example can be found at https://waspleau.netlify.app/ .
The deployed version of this example can be found at https://waspleau-app-client.fly.dev/ .
## Running in development

View File

@ -0,0 +1,17 @@
# fly.toml app configuration file generated for waspleau-app-client on 2023-10-13T09:31:45+02:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "waspleau-app-client"
primary_region = "mad"
[build]
[http_service]
internal_port = 8043
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

@ -0,0 +1,17 @@
# fly.toml app configuration file generated for waspleau-app-server on 2023-10-13T09:31:39+02:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "waspleau-app-server"
primary_region = "mad"
[build]
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

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

View File

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

View File

@ -0,0 +1,45 @@
# Using Websockets in Wasp
This is an example real-time, Websockets app built with Wasp in TypeScript to showcase the ease of use and integration of Websockets in Wasp. It's really NEAT!
[![wasp websockets app](image.png)](https://www.youtube.com/watch?v=Twy-2P0Co6M)
You can try out a deployed version of the app here: https://websockets-client-production.up.railway.app/
This app also includes Wasp's integrated auth and a voting system (again, neat!).
## Running the app
*If you get stuck at any point, feel free to join our [Discord server](https://discord.gg/rzdnErX) and ask questions there. We are happy to help!*
First, clone the this repo:
```bash
git clone https://github.com/wasp-lang/wasp.git
```
Make sure you've downloaded and installed Wasp
```bash
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
```
Then navigate to the project directory
```bash
cd examples/websockets-realtime-voting
```
```bash
wasp db migrate-dev
```
start the app! (this also installs all dependencies)
```bash
wasp start
```
Check out the `src/server/websocket.ts` and `src/client/pages/MainPage.tsx` to see how Websockets are used in Wasp.
## Need Help?
Wasp Docs: https://wasp-lang.dev/docs
Feel free to join our [Discord server](https://discord.gg/rzdnErX) and ask questions there. We are happy to help!

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

View File

@ -0,0 +1,45 @@
app whereDoWeEat {
wasp: {
version: "^0.11.6"
},
title: "where-do-we-eat",
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {}
}
},
dependencies: [
("flowbite", "1.6.6"),
("flowbite-react", "0.4.9")
],
webSocket: {
fn: import { webSocketFn } from "@server/ws-server.js",
}
}
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@client/MainPage.tsx",
authRequired: true
}
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@client/pages/LoginPage.jsx"
}
route RegisterRoute { path: "/signup", to: RegisterPage }
page RegisterPage {
component: import { SignupPage } from "@client/pages/SignupPage.jsx"
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

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

View File

@ -0,0 +1,71 @@
// @ts-check
import "./Main.css";
import { Flowbite, Dropdown, Navbar, Avatar } from "flowbite-react";
import Logo from "./logo.png";
import useAuth from "@wasp/auth/useAuth";
import logout from "@wasp/auth/logout";
const customTheme = {
button: {
color: {
primary: "bg-red-500 hover:bg-red-600",
},
}
};
export const Layout = ({ children }) => {
const { data: user } = useAuth();
return (
<Flowbite theme={{ theme: customTheme }}>
<div className="p-8">
<Navbar fluid rounded>
<Navbar.Brand className="cursor-pointer">
<img
alt="Fox Logo"
className="mr-3 h-6 sm:h-9"
src={Logo}
/>
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Undecisive Fox App
</span>
</Navbar.Brand>
{user && (
<div className="flex md:order-2">
<Dropdown
inline
label={
<Avatar
alt="User settings"
img={`https://xsgames.co/randomusers/avatar.php?g=female&username=${user.username}`}
rounded
/>
}
>
<Dropdown.Header>
<span className="block text-sm">{user.username}</span>
</Dropdown.Header>
<Dropdown.Item>Dashboard</Dropdown.Item>
<Dropdown.Item>Settings</Dropdown.Item>
<Dropdown.Item>Earnings</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item onClick={logout}>Sign out</Dropdown.Item>
</Dropdown>
<Navbar.Toggle />
</div>
)}
{/* <Navbar.Collapse>
<Navbar.Link active href="#">
<p>Home</p>
</Navbar.Link>
<Navbar.Link href="#">About</Navbar.Link>
<Navbar.Link href="#">Services</Navbar.Link>
<Navbar.Link href="#">Pricing</Navbar.Link>
<Navbar.Link href="#">Contact</Navbar.Link>
</Navbar.Collapse> */}
</Navbar>
<div className="grid place-items-center mt-8">{children}</div>
</div>
</Flowbite>
);
};

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,94 @@
import { useState, useMemo, useEffect } from "react";
import { Button, Card } from "flowbite-react";
import {
useSocketListener,
useSocket,
ServerToClientPayload,
} from "@wasp/webSocket";
import useAuth from "@wasp/auth/useAuth";
const MainPage = () => {
const { data: user } = useAuth();
const [poll, setPoll] = useState<ServerToClientPayload<"updateState"> | null>(
null
);
const totalVotes = useMemo(() => {
return (
poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0
);
}, [poll]);
const { socket } = useSocket();
useSocketListener("updateState", (newState) => {
setPoll(newState);
});
useEffect(() => {
socket.emit("askForStateUpdate");
}, []);
function handleVote(optionId: number) {
socket.emit("vote", optionId);
}
return (
<div className="w-full max-w-2xl mx-auto p-8">
<h1 className="text-2xl font-bold">{poll?.question ?? "Loading..."}</h1>
{poll && (
<p className="leading-relaxed text-gray-500">
Cast your vote for one of the options.
</p>
)}
{poll && (
<div className="mt-4 flex flex-col gap-4">
{poll.options.map((option) => (
<Card key={option.id} className="relative transition-all duration-300 min-h-[130px]">
<div className="z-10">
<div className="mb-2">
<h2 className="text-xl font-semibold">{option.text}</h2>
<p className="text-gray-700">{option.description}</p>
</div>
<div className="absolute bottom-5 right-5">
{user && !option.votes.includes(user.username) ? (
<Button onClick={() => handleVote(option.id)}>Vote</Button>
) : (
<Button disabled>Voted</Button>
)}
{!user}
</div>
{option.votes.length > 0 && (
<div className="mt-2 flex gap-2 flex-wrap max-w-[75%]">
{option.votes.map((vote) => (
<div
key={vote}
className="py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm"
>
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
<div className="text-gray-700">{vote}</div>
</div>
))}
</div>
)}
</div>
<div className="absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10">
{option.votes.length} / {totalVotes}
</div>
<div
className="absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300"
style={{
width: `${
totalVotes > 0
? (option.votes.length / totalVotes) * 100
: 0
}%`,
}}
></div>
</Card>
))}
</div>
)}
</div>
);
};
export default MainPage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,13 @@
import { LoginForm } from "@wasp/auth/forms/Login";
import { Link } from "react-router-dom";
export function LoginPage() {
return (
<div>
<LoginForm />
<div className="text-center mt-4">
<Link to="/signup">Sign up instead</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,5 @@
import { SignupForm } from "@wasp/auth/forms/Signup";
export function SignupPage() {
return <SignupForm />
}

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"
],
}

Some files were not shown because too many files have changed in this diff Show More