Merge branch 'main' into wasp-ai
2
.gitattributes
vendored
@ -1,3 +1,5 @@
|
||||
*.wasp linguist-language=JavaScript
|
||||
|
||||
*.hs linguist-detectable=false
|
||||
|
||||
/examples/* linguist-documentation
|
||||
|
@ -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
|
26
README.md
@ -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
@ -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
|
@ -12,7 +12,7 @@ app hackathonBetaSubmissions {
|
||||
}
|
||||
|
||||
entity Submission {=psl
|
||||
name String @id @unique
|
||||
name String @id @unique
|
||||
email String @unique
|
||||
github String
|
||||
description String
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
11
examples/streaming/.gitignore
vendored
Normal 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.*
|
21
examples/streaming/main.wasp
Normal 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",
|
||||
}
|
89
examples/streaming/src/client/Main.css
Normal 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;
|
||||
}
|
57
examples/streaming/src/client/MainPage.jsx
Normal 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());
|
||||
}
|
||||
}
|
55
examples/streaming/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"
|
||||
],
|
||||
}
|
1
examples/streaming/src/client/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />
|
BIN
examples/streaming/src/client/waspLogo.png
Normal file
After Width: | Height: | Size: 24 KiB |
29
examples/streaming/src/server/streaming.ts
Normal 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;
|
||||
};
|
48
examples/streaming/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"
|
||||
],
|
||||
}
|
@ -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"]
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
||||
|
17
examples/thoughts/fly-client.toml
Normal 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"]
|
17
examples/thoughts/fly-server.toml
Normal 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"]
|
@ -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],
|
||||
}
|
||||
|
@ -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
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
export type Task = {
|
||||
id: number;
|
||||
description: string;
|
||||
isDone: boolean;
|
||||
userId: number | null;
|
||||
};
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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[]>;
|
||||
|
@ -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<{}>;
|
||||
};
|
||||
};
|
@ -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
|
@ -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]
|
||||
}
|
||||
|
||||
|
@ -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");
|
@ -1,2 +0,0 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Excuse_text_key";
|
@ -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
|
Before Width: | Height: | Size: 199 KiB |
@ -1,5 +0,0 @@
|
||||
export const saveExcuse = async (excuse, context) => {
|
||||
return context.entities.Excuse.create({
|
||||
data: { text: excuse.text }
|
||||
})
|
||||
}
|
@ -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()
|
||||
}
|
@ -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]
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
7
examples/tutorials/TodoApp/src/client/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
})
|
@ -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 },
|
||||
})
|
||||
}
|
||||
|
@ -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
@ -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.*
|
1
examples/tutorials/TodoAppTs/.wasproot
Normal file
@ -0,0 +1 @@
|
||||
File marking the root of Wasp project.
|
69
examples/tutorials/TodoAppTs/main.wasp
Normal 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]
|
||||
}
|
@ -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
|
||||
);
|
@ -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");
|
@ -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;
|
3
examples/tutorials/TodoAppTs/src/.waspignore
Normal file
@ -0,0 +1,3 @@
|
||||
# Ignore editor tmp files
|
||||
**/*~
|
||||
**/#*#
|
16
examples/tutorials/TodoAppTs/src/client/LoginPage.tsx
Normal 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
|
83
examples/tutorials/TodoAppTs/src/client/MainPage.tsx
Normal 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
|
16
examples/tutorials/TodoAppTs/src/client/SignupPage.tsx
Normal 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
|
55
examples/tutorials/TodoAppTs/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"
|
||||
],
|
||||
}
|
1
examples/tutorials/TodoAppTs/src/client/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />
|
7
examples/tutorials/TodoAppTs/src/client/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
})
|
35
examples/tutorials/TodoAppTs/src/server/actions.ts
Normal 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 },
|
||||
})
|
||||
}
|
13
examples/tutorials/TodoAppTs/src/server/queries.ts
Normal 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' },
|
||||
})
|
||||
}
|
48
examples/tutorials/TodoAppTs/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"
|
||||
],
|
||||
}
|
@ -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"]
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
17
examples/waspleau/fly-client.toml
Normal 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"]
|
17
examples/waspleau/fly-server.toml
Normal 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"]
|
@ -1,3 +1,4 @@
|
||||
/.wasp/
|
||||
/.env.server
|
||||
/.env.client
|
||||
.DS_Store
|
1
examples/websockets-realtime-voting/.wasproot
Normal file
@ -0,0 +1 @@
|
||||
File marking the root of Wasp project.
|
45
examples/websockets-realtime-voting/README.md
Normal 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!
|
BIN
examples/websockets-realtime-voting/image.png
Normal file
After Width: | Height: | Size: 794 KiB |
45
examples/websockets-realtime-voting/main.wasp
Normal 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"
|
||||
}
|
6
examples/websockets-realtime-voting/postcss.config.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
3
examples/websockets-realtime-voting/src/.waspignore
Normal file
@ -0,0 +1,3 @@
|
||||
# Ignore editor tmp files
|
||||
**/*~
|
||||
**/#*#
|
71
examples/websockets-realtime-voting/src/client/Layout.jsx
Normal 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>
|
||||
);
|
||||
};
|
3
examples/websockets-realtime-voting/src/client/Main.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
94
examples/websockets-realtime-voting/src/client/MainPage.tsx
Normal 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;
|
BIN
examples/websockets-realtime-voting/src/client/logo.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { SignupForm } from "@wasp/auth/forms/Signup";
|
||||
|
||||
export function SignupPage() {
|
||||
return <SignupForm />
|
||||
}
|
55
examples/websockets-realtime-voting/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"
|
||||
],
|
||||
}
|