Merge branch 'release'

This commit is contained in:
Mihovil Ilakovac 2024-07-05 21:21:23 +02:00
commit 85dce3f153
15 changed files with 186 additions and 103 deletions

View File

@ -29,14 +29,17 @@ Why not? Well, serverless services turn on and off depending on if a request is
Luckily, were going to talk about two great ways you can implement them:
1. Implementing and configuring it yourself with React, NodeJS, and Socket.IO
2. By using [Wasp](https://wasp-lang.dev), a full-stack React-NodeJS framework, to configure and integrate Socket.IO into your app for you.
1. **Advanced**: Implementing and configuring it yourself with React, NodeJS, and Socket.IO
2. **Easy**: By using [Wasp](https://wasp-lang.dev), a full-stack React-NodeJS framework, to configure and integrate Socket.IO into your app for you.
These methods allow you to build fun stuff, like this instantly updating “voting with friends” app we built here (check out the [GitHub repo for it](https://github.com/vincanger/websockets-wasp)):
These methods allow you to build fun stuff, like this instantly updating “voting with friends” app we built here:
<!-- [https://www.youtube.com/watch?v=Twy-2P0Co6M](https://www.youtube.com/watch?v=Twy-2P0Co6M) -->
<iframe width="560" height="315" src="https://www.youtube.com/embed/Twy-2P0Co6M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
You can try out the [live demo app here](https://websockets-voting-client.fly.dev/).
And if you just want the app code, it's [available here on GitHub](https://github.com/vincanger/websockets-wasp).
## Why WebSockets?
So, imagine you're at a party sending text messages to a friend to tell them what food to bring.
@ -107,6 +110,8 @@ Thats why you need a “serverful” architecture if you want to build real-t
Okay, let's start with the first, more traditional approach: creating a dedicated server for your client to establish a two-way communication channel with.
This method is more advanced and involves a bit more complexity, but allows for more fine-tuned customization. **If you're looking for a straightforward, easier way to bring WebSockets to your React/NodeJS app, we'll get to that in the [section below](#implementing-websockets-with-wasp--easierless-config-method)**
:::note
👨‍💻 If you want to code along you can follow the instructions below. Alternatively, if you just want to see the finished React-NodeJS full-stack app, check out the [github repo here](https://github.com/vincanger/websockets-react)
:::
@ -499,14 +504,12 @@ Luckily, our next solution, Wasp, has integrated Authentication and Database Man
So lets go ahead and give that a go!
### Implementing WebSockets with Wasp — Fast/Zero Config Method
### Implementing WebSockets with Wasp — Easier/Less Config Method
Because Wasp is an innovative full-stack framework, it makes building React-NodeJS apps quick and developer-friendly.
Wasp has lots of time-saving features, including WebSocket support via [Socket.IO](http://socket.io/), Authentication, Database Management, and Full-stack type-safety out-of-the box.
<!-- [https://twitter.com/WaspLang/status/1673742264873500673?s=20](https://twitter.com/WaspLang/status/1673742264873500673?s=20) -->
Wasp can take care of all this heavy lifting for you because of its use of a config file, which you can think of like a set of instructions that the Wasp compiler uses to help glue your app together.
To see it in action, let's implement WebSocket communication using Wasp by following these steps
@ -518,7 +521,7 @@ If you just want to see finished apps code, you can check out the [GitHub rep
1. Install Wasp globally by running the following command in your terminal:
```bash
curl -sSL [https://get.wasp-lang.dev/installer.sh](https://get.wasp-lang.dev/installer.sh) | sh
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
```
If you want to code along, first clone the `start` branch of the example app:
@ -538,13 +541,13 @@ Lets start out by taking a quick look at our `main.wasp` file.
```jsx
app whereDoWeEat {
wasp: {
version: "^0.11.0"
version: "^0.13.2"
},
title: "where-do-we-eat",
client: {
rootComponent: import { Layout } from "@client/Layout.jsx",
rootComponent: import { Layout } from "@src/client/Layout",
},
// 🔐 this is how we get auth in our app.
// 🔐 This is how we get Auth in our app. Easy!
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
@ -552,17 +555,11 @@ app whereDoWeEat {
usernameAndPassword: {}
}
},
dependencies: [
("flowbite", "1.6.6"),
("flowbite-react", "0.4.9")
]
}
// 👱 this is the data model for our registered users in our database
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}
// ...
@ -576,7 +573,7 @@ Lets tell it we want WebSockets, as well. Add the `webSocket` definition to t
app whereDoWeEat {
// ...
webSocket: {
fn: import { webSocketFn } from "@server/ws-server.js",
fn: import { webSocketFn } from "@src/server/ws-server",
},
// ...
}
@ -585,11 +582,9 @@ app whereDoWeEat {
Now we have to define the `webSocketFn`. In the `./src/server` directory create a new file, `ws-server.ts` and copy the following code:
```tsx
import { WebSocketDefinition } from '@wasp/webSocket';
import { User } from '@wasp/entities';
import { getUsername } from 'wasp/auth';
import { type WebSocketDefinition } from 'wasp/server/webSocket';
// define the types. this time we will get the entire User object
// in SocketData from the Auth that Wasp automatically sets up for us 🎉
type PollState = {
question: string;
options: {
@ -599,6 +594,7 @@ type PollState = {
votes: string[];
}[];
};
interface ServerToClientEvents {
updateState: (state: PollState) => void;
}
@ -607,18 +603,11 @@ interface ClientToServerEvents {
askForStateUpdate: () => void;
}
interface InterServerEvents {}
interface SocketData {
user: User;
}
// pass the generic types to the websocketDefinition just like
// in the previous example
export const webSocketFn: WebSocketDefinition<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
> = (io, _context) => {
export const webSocketFn: WebSocketDefinition<ClientToServerEvents, ServerToClientEvents, InterServerEvents> = (
io,
_context
) => {
const poll: PollState = {
question: "What are eating for lunch ✨ Let's order",
options: [
@ -648,27 +637,32 @@ export const webSocketFn: WebSocketDefinition<
return;
}
console.log('Socket connected: ', socket.data.user?.username);
const connectionUsername = getUsername(socket.data.user);
console.log('Socket connected: ', connectionUsername);
socket.on('askForStateUpdate', () => {
socket.emit('updateState', poll);
});
socket.on('vote', (optionId) => {
if (!connectionUsername) {
return;
}
// If user has already voted, remove their vote.
poll.options.forEach((option) => {
option.votes = option.votes.filter((username) => username !== socket.data.user.username);
option.votes = option.votes.filter((username) => username !== connectionUsername);
});
// And then add their vote to the new option.
const option = poll.options.find((o) => o.id === optionId);
if (!option) {
return;
}
option.votes.push(socket.data.user.username);
option.votes.push(connectionUsername);
io.emit('updateState', poll);
});
socket.on('disconnect', () => {
console.log('Socket disconnected: ', socket.data.user?.username);
console.log('Socket disconnected: ', connectionUsername);
});
});
};
@ -717,63 +711,53 @@ Lets take a look at how that works now.
In `.src/client/MainPage.tsx`, replace the contents with the following code:
```tsx
import { useState, useMemo, useEffect } from "react";
import { Button, Card } from "flowbite-react";
// Wasp provides us with pre-configured hooks and types based on
// our server code. No need to set it up ourselves!
import {
useSocketListener,
useSocket,
ServerToClientPayload,
} from "@wasp/webSocket";
import useAuth from "@wasp/auth/useAuth";
import { type ServerToClientPayload, useSocket, useSocketListener } from 'wasp/client/webSocket';
import { useAuth } from 'wasp/client/auth';
import { useState, useMemo, useEffect } from 'react';
import { Button, Card } from 'flowbite-react';
import { getUsername } from 'wasp/auth';
const MainPage = () => {
// we can easily access the logged in user with this hook
// that wasp provides for us
// Wasp provides a bunch of pre-built hooks for us :)
const { data: user } = useAuth();
const [poll, setPoll] = useState<ServerToClientPayload<"updateState"> | null>(
null
);
const [poll, setPoll] = useState<ServerToClientPayload<'updateState'> | null>(null);
const totalVotes = useMemo(() => {
return (
poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0
);
return poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0;
}, [poll]);
// pre-built hooks, configured for us by Wasp
const { socket } = useSocket();
useSocketListener("updateState", (newState) => {
const { socket } = useSocket();
const username = user ? getUsername(user) : null;
useSocketListener('updateState', (newState) => {
setPoll(newState);
});
useEffect(() => {
socket.emit("askForStateUpdate");
socket.emit('askForStateUpdate');
}, []);
function handleVote(optionId: number) {
socket.emit("vote", optionId);
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>
<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 && (
<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">
<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>
<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) ? (
<div className='absolute bottom-5 right-5'>
{username && !option.votes.includes(username) ? (
<Button onClick={() => handleVote(option.id)}>Vote</Button>
) : (
<Button disabled>Voted</Button>
@ -781,30 +765,28 @@ const MainPage = () => {
{!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 className='mt-2 flex gap-2 flex-wrap max-w-[75%]'>
{option.votes.map((username, idx) => {
return (
<div
key={username}
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'>{username}</div>
</div>
);
})}
</div>
)}
</div>
<div className="absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10">
<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"
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
}%`,
width: `${totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0}%`,
}}
></div>
</Card>

View File

@ -0,0 +1,99 @@
---
title: 'Building and Selling a GPT Wrapper SaaS in 5 Months'
authors: [vinny]
image: /img/selling-saas.png
tags: [saas, boilerplate, acquire, react, gpt, openai, marketing]
---
Since the release of ChatGPT, weve been flooded with all possible versions of apps that use it in one way or another. Building on top of trendy technology is an excellent way to get initial attention, but still, 99% of these apps die very quickly and dont last beyond a week or two following their “big” Twitter or Product Hunt launch.
Why? **Because they arent solving a real problem**. Its either a fun tech gadget or a gross overpromise (e.g., *“you will never need to code again,”* which [I strongly disagree with](https://wasp-lang.dev/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future)) that quickly falls short.
Building a successful product still follows the same rules as in the pre-GPT era: **find a problem people are willing to pay for and then figure out a way to reach these people**. Sounds simple? It is, but it for sure isnt easy. The good news is that GPT opened so many new opportunities that actually doing it is faster and easier than ever.
## Meet the hero of our story - Max! 🦸
![our-hero-max](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rowl3gqdrb2nhhdl1hyo.gif)
The hero of our story today is Max, a software engineer at Red Hat. He built [https://description-generator.online](https://description-generator.online/) (an AI description generator for Etsy products) and sold it on [acquire.com](http://acquire.com). A senior backend engineer by day and a serial hacker and tinkerer by night, Max always had a passion for building products, and GPT was the last piece of the puzzle he was waiting for.
Read on to learn how he went through the entire cycle of finding a problem, building a solution, getting customers, and ultimately selling his app in 5 months total.
## Lesson #1: Look for problems in “unusual” places 🕵️‍♂️
![Looking for problems](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kz3nsmm5zheyjukvr6j8.gif)
*TL;DR: Talk to your friends who arent developers! Learn about their problems and offer help. The more unfamiliar and disconnected from tech their occupation is, the better - these are gold mines for GPT-powered solutions!*
It all started with Maxs friend who owns an Etsy marketplace - she needed help with some data/workflow automation, and Max agreed to lend a hand. Consequently, he also started hanging out in the Ukranian Etsy community on Slack.
Soon, he learned that one of the most common requests there is for help with writing product descriptions (”listings”) in English. Although most members used English daily and had no problem communicating, writing high-quality, compelling, and professional-sounding listings was still a challenge. Auto-translation services still werent sophisticated enough, and hiring native English speakers was too expensive for most.
This sounded like a real, glaring problem directly connected to the number of items Etsy sellers sell and, thus, the profit they make. As it turned out, it was the case.
## Lesson #2: Build a prototype, fast 🏎️
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ign7kz6464wnuac13y8.gif)
*TL;DR: Speed is the name of the game here. Dont spend time flexing on your stack and optimizing to the last byte. Pick something that works and ship it!*
The problem of writing convincing product listings in English caught Maxs attention. He was aware of ChatGPT and how useful it could be for this. However, being a backend engineer with limited frontend experience, building a full-stack app around it and choosing and configuring all parts of the stack himself sounded daunting and laborious. It wasnt until he came across [Open SaaS](https://opensaas.sh) that he felt ready to take action.
The prototype was ready after a couple of days, and Max immediately shared it with his Etsy community. He kept it extremely simple - no landing page or any copy at all (just a form to enter your product details), even no custom domain yet, but myProduct.fly.io you get assigned upon deploying to Fly (which takes just a single CLI command with Wasp).
And that was enough - as his product scratched the itch Etsy sellers repeatedly mentioned, the reception was overwhelmingly positive! In just a few days, Max got 400 signups, and several hundred product listings were generated daily.
---
By the way, if youre looking for an easy, low maintenance way to start your next side project, check out [Open SaaS](https://opensaas.sh/), a 100% free, open-source Saas Starter!
![https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png)
Open SaaS is a feature-rich, React + NodeJS SaaS template, with Stripe, OpenAI / GPT app examples, AWS S3 file upload, Analytics, Admin Dashboard, and full Documentation!
---
## Lesson #3: Test willingness to pay early 💸
![money please](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6rcbozh1euokgyvgv1g.gif)
*TL;DR: People signing up for your product is amazing, but convincing them to pay is a completely separate game. If you want to ensure your solution brings real value and youre not wasting time, find a way to test monetizing as early as possible.*
Max saw the adoption picking up, which made him ask himself *“How do I turn this into a business? What would users be willing to pay for?”* After all, he had his own expenses, like server costs and GPT API subscription.
Looking at how users use the product, he quickly realized he could make generating descriptions even easier - a seller could upload the image of a product, and thats it; the full product description can be generated directly from it. That was a good candidate for a “premium” feature, since it was an upgrade on top of the basic functionality.
Max added the feature, and soon enough, the first customers started rolling in! 💰
## Lesson #4: Keep building or sell? How to decide 🤔
![homer selling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sm55hit6negsr6emmgrg.gif)
*TL;DR: Is the markets domain something youre personally excited about and see yourself in for the long term? Do you feel your competitive advantage will grow stronger with time? If yes, keep building. Otherwise, sell!*
[description-generator.online](http://Decision-generator.online) now had both users and first revenue, amazing! Still, soon, it became apparent that the Etsy community Max was part of had its limits. Although all non-English speaking markets shared the problem, which made for a big opportunity, reaching them and setting up and executing a sales process would still take time and effort.
On the other hand, competing products started appearing. Although super valuable for Etsy sellers, if Max built the product in a week, others could do it too. It started becoming clear that the value of the business would soon start moving from the technical solution to sales, support, and customer experience.
Being a hacker at heart and not so personally invested in arts & crafts marketplaces, Max decided to sell the product to somebody who is. He listed the description generator on https://acquire.com/, along with the usage metrics and relevant data, and soon started receiving offers.
## Lesson #5: Provide support during acquisition 🤝
![got my back](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6ay41rsjabkv9mkqcto.gif)
*TL;DR: Selling your product takes more than finding a buyer. Providing impeccable support during acquisition is just as important as building the product.*
Finding a buyer and agreeing on a price took about a month. Since the buyer was taking over everything - the source code, domain, and customers, Max providing 3-month support with the transition was an essential part of the deal.
Also, since they couldnt use an escrow service due to some technical and geographical limitations, they agreed on splitting the payment 50/50 - half in the beginning and another half when the migration was over. Max made sure his customers had a flawless experience with moving everything over, resulting in a great relationship mutually filled with trust. Besides selling your app, making friends is an underrated bonus! 😎
After a few months, the deal has been reached! [Description-generator.online](http://description-generator.online/) got a new owner, an expert in the industry willing to expand to new markets, and Max got his first exit and could move on to the next exciting project!
## Summary
![michael summary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6zioe34s0u9ic80g26k.gif)
Thats it! Building a product others find helpful so much theyre willing to pay for it is a deeply gratifying experience. We saw how Max did it and what lessons he learned along the way:
1. Look for problems in “unusual” places
2. Build a prototype fast
3. Test willingness to pay early
4. Decide whether you want to keep building or sell
5. Provide support during the acquisition
Hopefully, this was helpful!

View File

@ -273,7 +273,7 @@ Make sure you set this URL as the `WASP_WEB_CLIENT_URL` environment variable in
We will show how to deploy the client, the server, and provision a database on Railway.
Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their [plans page](https://docs.railway.app/reference/plans) for more info.
Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their [plans page](https://docs.railway.app/reference/pricing/plans) for more info.
### Prerequisites
@ -453,7 +453,7 @@ Railway will now locate the Dockerfile and deploy your server 👍
5. Set the `PORT` environment variable to `8043` under the `Variables` tab.
6. Deploy the client and select `client` when prompted with `Select Service`:
6. Once set, deploy the client and select `client` when prompted with `Select Service`:
```shell
railway up

View File

@ -224,7 +224,7 @@ Then, get the Mailgun API key and domain and add them to your `.env.server` file
1. Go to [Mailgun](https://www.mailgun.com/) and create an account.
2. Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key.
3. Copy the API key and add it to your `.env.server` file.
4. Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain.
4. Go to [Domains](https://app.mailgun.com/mg/sending/domains) and create a new domain.
5. Copy the domain and add it to your `.env.server` file.
```properties title=".env.server"

View File

@ -162,14 +162,14 @@ export const getFilteredTasks: GetFilteredTasks<
}
```
Wasp automatically generates the types `GetTasks` and `GetFilteredTasks` based on your Wasp file's declarations:
Wasp automatically generates the types `GetAllTasks` and `GetFilteredTasks` based on your Wasp file's declarations:
- `GetTasks` is a generic type automatically generated by Wasp, based on the Query declaration for `getTasks`.
- `GetAllTasks` is a generic type automatically generated by Wasp, based on the Query declaration for `getAllTasks`.
- `GetFilteredTasks` is also a generic type automatically generated by Wasp, based on the Query declaration for `getFilteredTasks`.
You can utilize these types to define the input and output types for your Query.
For example, the Query `getTasks` doesn't expect any arguments (its input type is `void`), but it does return a list of tasks (its output type is `Task[]`).
For example, the Query `getAllTasks` doesn't expect any arguments (its input type is `void`), but it does return a list of tasks (its output type is `Task[]`).
On the other hand, the Query `getFilteredTasks` expects an object of type `{ isDone: boolean }`. This type is derived from the `Task` type.

View File

@ -31,7 +31,7 @@ const examples = [
authorImg: 'https://avatars.githubusercontent.com/u/55102317',
repoName: 'websockets-realtime-voting',
repoUrl: 'https://github.com/wasp-lang/wasp/tree/release/examples/websockets-realtime-voting',
demoUrl: 'https://websockets-client-production.up.railway.app/login',
demoUrl: 'https://websockets-voting-client.fly.dev/login',
},
]

View File

@ -79,6 +79,7 @@ export default function BlogPostItems({
{/* Image - right col */}
<img
loading="lazy"
src={coverImgSrc}
className={`
mb-6
@ -121,6 +122,7 @@ export default function BlogPostItems({
>
{/* Image */}
<img
loading="lazy"
src={coverImgSrc}
className={`
mb-6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -271,7 +271,7 @@ Make sure you set this URL as the `WASP_WEB_CLIENT_URL` environment variable in
We will show how to deploy the client, the server, and provision a database on Railway.
Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their [plans page](https://docs.railway.app/reference/plans) for more info.
Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their [plans page](https://docs.railway.app/reference/pricing/plans) for more info.
### Prerequisites
@ -451,7 +451,7 @@ Railway will now locate the Dockerfile and deploy your server 👍
5. Set the `PORT` environment variable to `8043` under the `Variables` tab.
6. Deploy the client and select `client` when prompted with `Select Service`:
6. Once set, deploy the client and select `client` when prompted with `Select Service`:
```shell
railway up

View File

@ -224,7 +224,7 @@ Then, get the Mailgun API key and domain and add them to your `.env.server` file
1. Go to [Mailgun](https://www.mailgun.com/) and create an account.
2. Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key.
3. Copy the API key and add it to your `.env.server` file.
4. Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain.
4. Go to [Domains](https://app.mailgun.com/mg/sending/domains) and create a new domain.
5. Copy the domain and add it to your `.env.server` file.
```properties title=".env.server"

View File

@ -162,14 +162,14 @@ export const getFilteredTasks: GetFilteredTasks<
}
```
Wasp automatically generates the types `GetTasks` and `GetFilteredTasks` based on your Wasp file's declarations:
Wasp automatically generates the types `GetAllTasks` and `GetFilteredTasks` based on your Wasp file's declarations:
- `GetTasks` is a generic type automatically generated by Wasp, based on the Query declaration for `getTasks`.
- `GetAllTasks` is a generic type automatically generated by Wasp, based on the Query declaration for `getAllTasks`.
- `GetFilteredTasks` is also a generic type automatically generated by Wasp, based on the Query declaration for `getFilteredTasks`.
You can utilize these types to define the input and output types for your Query.
For example, the Query `getTasks` doesn't expect any arguments (its input type is `void`), but it does return a list of tasks (its output type is `Task[]`).
For example, the Query `getAllTasks` doesn't expect any arguments (its input type is `void`), but it does return a list of tasks (its output type is `Task[]`).
On the other hand, the Query `getFilteredTasks` expects an object of type `{ isDone: boolean }`. This type is derived from the `Task` type.