diff --git a/web/docs/introduction/getting-started.md b/web/docs/introduction/getting-started.md index 4f7ee0c83..fe97fd2ae 100644 --- a/web/docs/introduction/getting-started.md +++ b/web/docs/introduction/getting-started.md @@ -4,8 +4,6 @@ slug: /quick-start next: /tutorials/todo-app --- -import Tabs from '@theme/Tabs' -import TabItem from '@theme/TabItem' import useBaseUrl from '@docusaurus/useBaseUrl'; ## Installation diff --git a/web/docs/language/features.md b/web/docs/language/features.md index 8c88abb9d..5256ed136 100644 --- a/web/docs/language/features.md +++ b/web/docs/language/features.md @@ -2,8 +2,6 @@ title: Features --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md' ## App diff --git a/web/docs/tutorials/todo-app.md b/web/docs/tutorials/todo-app.md index 6b3618370..5bf0554d5 100644 --- a/web/docs/tutorials/todo-app.md +++ b/web/docs/tutorials/todo-app.md @@ -5,10 +5,10 @@ title: Introduction import useBaseUrl from '@docusaurus/useBaseUrl'; :::info -If you want to run Wasp locally and haven't yet set it up, check out [Getting Started](/docs) first for installation instructions, and then continue with the tutorial. +If you want to run Wasp locally and haven't yet set it up, check out [Quick Start](/docs/quick-start) first for installation instructions, and then continue with the tutorial. ::: -Let's build together a simple Todo App in Wasp! +Let's build a simple Todo App in Wasp! How Todo App will work once it is done + + ```wasp title="main.wasp" app TodoApp { // Main declaration, defines a new web app. wasp: { @@ -93,7 +92,7 @@ app TodoApp { // Main declaration, defines a new web app. title: "Todo app" // Used as a browser tab title. } -route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/` (default url). +route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/` page MainPage { // We specify that ReactJS implementation of our page can be found in @@ -103,7 +102,40 @@ page MainPage { } ``` + + + +```wasp title="main.wasp" +app TodoApp { // Main declaration, defines a new web app. + wasp: { + version: "^0.11.0" + }, + title: "Todo app" // Used as a browser tab title. +} + +route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/` + +page MainPage { + // We specify that ReactJS implementation of our page can be found in + // `src/client/MainPage.tsx` as a default export (uses standard JS import syntax). + // Use '@client' to reference files inside the src/client folder. + component: import Main from "@client/MainPage.tsx" +} +``` + + + + + +:::warning Using TypeScript +If you are using Typescript, you will need to rename `MainPage.jsx` to `MainPage.tsx`. +::: + + Let's now take a look at that React component we referenced in the `page MainPage { ... }` declaration in `main.wasp`: + + + ```jsx title="src/client/MainPage.jsx" import waspLogo from './waspLogo.png' import './Main.css' @@ -113,6 +145,22 @@ const MainPage = () => { } export default MainPage ``` + + + +```tsx title="src/client/MainPage.tsx" +import waspLogo from './waspLogo.png' +import './Main.css' + +const MainPage = () => { + ... +} +export default MainPage +``` + + + + As we can see, this is simply a functional React component that uses the CSS and Wasp logo files sitting next to it in the `src/client` dir. This is all the code we need! @@ -133,14 +181,32 @@ IDE support, so it's important to keep them. Next, let's make the `MainPage` component much simpler: + + + + ```jsx title="src/client/MainPage.jsx" const MainPage = () => { - return
Hello world!
+ return
Hello world!
} export default MainPage ``` +
+ + +```tsx title="src/client/MainPage.tsx" +const MainPage = () => { + return
Hello world!
+} + +export default MainPage +``` + +
+
+ At this point, you should see something like this: Todo App - Hello World + It consists of a declaration in Wasp and implementation in JS (in `src/server/` directory). + + + +It consists of a declaration in Wasp and implementation in TS (in `src/server/` directory). + ### Wasp declaration Add the following code to `main.wasp`: + + + + ```wasp title="main.wasp" // ... @@ -38,6 +51,24 @@ query getTasks { entities: [Task] } ``` + + + + +```wasp title="main.wasp" +// ... + +query getTasks { + // We specify that JS implementation of the query (which is an async JS function) + // can be found in `src/server/queries.js` as the named export `getTasks`. + // Use '@server' to reference files inside the src/server folder. + fn: import { getTasks } from "@server/queries.js", + // We tell Wasp that this query is doing something with entity `Task`. With that, Wasp will + // automatically refresh the results of this query when tasks change. + entities: [Task] +} +``` + :::caution Even if you use TypeScript and have the file `queries.ts`, you will still need to import it using the `.js` extension. Wasp internally uses `esnext` module resolution, which always requires specifying the extension as `.js` (i.e., the extension used in the emitted JS file). This applies to all `@server` imports (and files on the server in general). It does not apply to client files. @@ -45,8 +76,26 @@ Even if you use TypeScript and have the file `queries.ts`, you will still need t Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588). ::: -### JS implementation -Next, create a new file `src/server/queries.js` and define the JS we've just used in the `query` declaration above: + + + + + +### JavaScript implementation + +Next, create a new file `src/server/queries.ts` and define the JavaScript function we've just imported in our `query` declaration: + + + +### TypeScript implementation + +Next, create a new file `src/server/queries.ts` and define the TypeScript function we've just imported in our `query` declaration: + + + + + + ```js title="src/server/queries.js" export const getTasks = async (args, context) => { @@ -54,6 +103,29 @@ export const getTasks = async (args, context) => { } ``` + + + +```js title="src/server/queries.ts" +import { Task } from "@wasp/entities" +import { GetTasks } from "@wasp/queries/types" + +export const getTasks: GetTasks = async (args, context) => { + return context.entities.Task.findMany({}) +} +``` + +Wasp automatically generates the types `GetTasks` and `Task` based the contents of `main.wasp`: + - `Task` is a type corresponding to the `Task` entity we've defined in `main.wasp`. + - `GetTasks` is a generic type Wasp automatically generated based the `getTasks` query we've defined in `main.wasp`. + +You can use these types to specify the Query's input and output types. This Query doesn't expect any arguments (meaning that its input type is `void`), but it does return an array of tasks (meaning that its output type is `Task[]`) + +Annotating the Queries is optional, but highly recommended because doing so enables **full-stack type safety**. We'll see what this means in the next section. + + + + Query function parameters: - `args`: `object`, arguments the query is invoked with. - `context`: `object`, additional stuff provided by Wasp. @@ -65,52 +137,125 @@ Since we declared in `main.wasp` that our query uses the `Task` entity, Wasp inj Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the `src/server` folder. ::: -## Invoking the query from React +## Invoking the Query on the frontend -We've just said that the queries we write are executed on the server, but Wasp will generate client-side query functions (taking care of the network and cache invalidation in the background). Let's finally use the query we've just created, `getTasks`, in our React component to list the tasks: +We've just said that the queries we write are executed on the server, but Wasp will generate client-side query functions (taking care of serialization, network calls, and cache invalidation in the background). Let's finally use the query we've just created, `getTasks`, in our React component to list the tasks: -```jsx {1-2,5-14,17-32} title="src/client/MainPage.jsx" -import getTasks from '@wasp/queries/getTasks' -import { useQuery } from '@wasp/queries' + + + +```jsx {1-2,5-14,17-36} title="src/client/MainPage.jsx" +import getTasks from "@wasp/queries/getTasks" +import { useQuery } from "@wasp/queries" const MainPage = () => { - const { data: tasks, isFetching, error } = useQuery(getTasks) + const { data: tasks, isLoading, error } = useQuery(getTasks) return (
{tasks && } - {isFetching && 'Fetching...'} - {error && 'Error: ' + error} + {isLoading && "Loading..."} + {error && "Error: " + error}
) } -const Task = (props) => { +const Task = ({ task }) => { return (
- - {props.task.description} + + {task.description}
) } -const TasksList = (props) => { - if (!props.tasks?.length) return 'No tasks' - return props.tasks.map((task, idx) => ) +const TasksList = ({ tasks }) => { + if (!tasks?.length) return
No tasks
+ + return ( +
+ {tasks.map((task, idx) => ( + + ))} +
+ ) } export default MainPage ``` -Most of this is just regular React, the only exception being two special `@wasp` imports: +
+ + + +```tsx {1-3,6-15,18-37} title="src/client/MainPage.tsx" +import getTasks from "@wasp/queries/getTasks" +import { useQuery } from "@wasp/queries" +import { Task } from "@wasp/entities" + +const MainPage = () => { + const { data: tasks, isLoading, error } = useQuery(getTasks) + + return ( +
+ {tasks && } + + {isLoading && "Loading..."} + {error && "Error: " + error} +
+ ) +} + +const Task = ({ task }: { task: Task }) => { + return ( +
+ + {task.description} +
+ ) +} + +const TasksList = ({ tasks }: { tasks: Task[] }) => { + if (!tasks?.length) return
No tasks
+ + return ( +
+ {tasks.map((task, idx) => ( + + ))} +
+ ) +} + +export default MainPage +``` + +
+
+ + + +Most of this code is just regular React, the only exception being the two special `@wasp` imports: - `import getTasks from '@wasp/queries/getTasks'` - Gives us our freshly defined Wasp query. - `import { useQuery } from '@wasp/queries'` - Gives us Wasp's [useQuery](language/features.md#the-usequery-hook) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query)'s [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp. -While we could directly call the query with `getTasks()`, calling it with `useQuery(getTasks)` gives us reactivity - the React component gets re-rendered if the result of the query changes. + + + + +Most of this code is just regular React, the only exception being the three special `@wasp` imports: + - `import getTasks from '@wasp/queries/getTasks'` - Gives us our freshly defined Wasp query. + - `import { useQuery } from '@wasp/queries'` - Gives us Wasp's [useQuery](language/features.md#the-usequery-hook) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query)'s [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp. + - `import { Task } from '@wasp/entities'` - The type for Task entity we've defined in `main.wasp`. + +Notice how you didn't need to tell TypeScript anything about the Query's response data - TypeScript inferred it automatically. + +Because we've previously annotated the Query's backend implementation with `GetTasks`, Wasp knows the response data's correct type (i.e., `Task[]`) on the frontend. We call this feature **full-stack type safety**. + + + +We could have called the Query directly with `getTasks()`, but wrapping it with `useQuery(getTasks)` makes it reactive. More precisely, React will re-render the component every time the Query's result changes. With these changes, you should be seeing the text "No tasks" on the screen: diff --git a/web/docs/tutorials/todo-app/04-creating-tasks.md b/web/docs/tutorials/todo-app/04-creating-tasks.md index ceee1e91f..bd946a6ae 100644 --- a/web/docs/tutorials/todo-app/04-creating-tasks.md +++ b/web/docs/tutorials/todo-app/04-creating-tasks.md @@ -4,17 +4,22 @@ title: "Creating tasks" --- import useBaseUrl from '@docusaurus/useBaseUrl'; +import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'; To enable the creation of new tasks, we will need two things: 1. A Wasp action that creates a new task. 2. A React form that calls that action with the new task's data. -## Action +## Defining the Action Creating an action is very similar to creating a query. ### Wasp declaration -First, we declare the action in Wasp: +We must first declare the Action in `main.wasp`: + + + + ```wasp title="main.wasp" // ... @@ -24,9 +29,39 @@ action createTask { } ``` -### JS implementation + + + +```wasp title="main.wasp" +// ... + +action createTask { + fn: import { createTask } from "@server/actions.js", + entities: [Task] +} +``` + + + + + + + +### JavaScript implementation +Let's now define a JavaScript function for our action: + + + + + +### TypeScript implementation +Let's now define a TypeScript function for our action: + + + + + -Next, we define a JS function for that action: ```js title="src/server/actions.js" export const createTask = async (args, context) => { return context.entities.Task.create({ @@ -35,19 +70,47 @@ export const createTask = async (args, context) => { } ``` + + + + +```ts title="src/server/actions.ts" +import { Task } from "@wasp/entities" +import { CreateTask } from "@wasp/actions/types" + +type CreateTaskPayload = Pick + +export const createTask: CreateTask = async ( + args, + context +) => { + return context.entities.Task.create({ + data: { description: args.description }, + }) +} +``` +Once again, we've annotated the Action with proper types (using the types `Task` and `CreateTask` Wasp generated for us). Annotating the Action makes the type information automatically available the frontend, giving us automatic **full-stack type safety**. + + + + :::tip -We put the JS function in a new file `src/server/actions.js`, but we could have put it anywhere we wanted! There are no limitations here, as long as the import statement in the Wasp file is correct and the source file is inside the `src/server` folder. +We put the function in a new file `src/server/actions.{js,ts}`, but we could have put it anywhere we wanted! There are no limitations here, as long as the import statement in the Wasp file is correct and the source file is inside the `src/server` folder. ::: -## React form +## Invoking the Action on the frontend -```jsx {3,10,37-59} title="src/client/MainPage.jsx" -import { useQuery } from '@wasp/queries' -import getTasks from '@wasp/queries/getTasks' -import createTask from '@wasp/actions/createTask' + + + + +```jsx {3,10,41-60} title="src/client/MainPage.jsx" +import getTasks from "@wasp/queries/getTasks" +import createTask from "@wasp/actions/createTask" +import { useQuery } from "@wasp/queries" const MainPage = () => { - const { data: tasks, isFetching, error } = useQuery(getTasks) + const { data: tasks, isLoading, error } = useQuery(getTasks) return (
@@ -55,49 +118,50 @@ const MainPage = () => { {tasks && } - {isFetching && 'Fetching...'} - {error && 'Error: ' + error} + {isLoading && "Loading..."} + {error && "Error: " + error}
) } -const Task = (props) => { +const Task = ({ task }) => { return (
- - {props.task.description} + + {task.description}
) } -const TasksList = (props) => { - if (!props.tasks?.length) return 'No tasks' - return props.tasks.map((task, idx) => ) +const TasksList = ({ tasks }) => { + if (!tasks?.length) return
No tasks
+ + return ( +
+ {tasks.map((task, idx) => ( + + ))} +
+ ) } -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) + } catch (err: any) { + window.alert("Error: " + err.message) } } return (
- - + +
) } @@ -105,9 +169,90 @@ const NewTaskForm = (props) => { export default MainPage ``` -Here we call our action directly (no hooks) because we don't need any reactivity. The rest is just regular React code. +
+ + +```tsx {1,3,12,43-62} title="src/client/MainPage.tsx" +import { FormEvent } from "react" +import getTasks from "@wasp/queries/getTasks" +import createTask from "@wasp/actions/createTask" +import { useQuery } from "@wasp/queries" +import { Task } from "@wasp/entities" + +const MainPage = () => { + const { data: tasks, isLoading, error } = useQuery(getTasks) + + return ( +
+ + + {tasks && } + + {isLoading && "Loading..."} + {error && "Error: " + error} +
+ ) +} + +const Task = ({ task }: { task: Task }) => { + return ( +
+ + {task.description} +
+ ) +} + +const TasksList = ({ tasks }: { tasks: Task[] }) => { + if (!tasks?.length) return
No tasks
+ + return ( +
+ {tasks.map((task, idx) => ( + + ))} +
+ ) +} + +const NewTaskForm = () => { + const handleSubmit = async (event: FormEvent) => { + 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 ( +
+ + +
+ ) +} + +export default MainPage +``` + +
+
+ +We're calling the `createTask` Action directly this time (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code. + + + + +Finally, because we've previously annotated the Action's backend implementation with the correct type, Wasp knows that the `createTask` action expects a value of type `{ description: string }` (try changing the argument and reading the error message). Wasp also knows that a call to the `createTask` action returns a `Task`, but we don't need it. + + That's it! + Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and also saved in the database. Try refreshing the page or opening it in another browser - you'll see the tasks are still here! diff --git a/web/docs/tutorials/todo-app/05-updating-tasks.md b/web/docs/tutorials/todo-app/05-updating-tasks.md index be45423c3..799278e1f 100644 --- a/web/docs/tutorials/todo-app/05-updating-tasks.md +++ b/web/docs/tutorials/todo-app/05-updating-tasks.md @@ -4,18 +4,23 @@ title: "Updating tasks" --- import useBaseUrl from '@docusaurus/useBaseUrl'; +import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'; -The Todo app isn't done if you can't mark a task as done! +Our Todo app isn't finished if you we can't mark a task as finished! -For that, we will need to do two things: -1. Implement a Wasp action that updates the task. -2. Modify our React code so it calls that action. +To do that, we'll need to do two things: +1. Implement a Wasp Action for toggling a task's `isDone` state. +2. Call this Action from React whenever the user toggles a checkbox. -## Action +## Defining the Action ### Wasp declaration -We declare a Wasp action: +Let's first define the Action in `main.wasp`: + + + + ```wasp title="main.wasp" // ... @@ -25,55 +30,159 @@ action updateTask { } ``` -### JS implementation + + + +```wasp title="main.wasp" +// ... + +action updateTask { + fn: import { updateTask } from "@server/actions.js", + entities: [Task] +} +``` + + + + + + + + +### JavaScript implementation +Let's now define the Action's JavaScript implementation in `src/server/actions.js`: + + + + + +### TypeScript implementation +Let's now define the Action's JavaScript implementation in `src/server/actions.ts`: + + + + + -We define the JS implementation of the Wasp action in `src/server/actions.js`: ```js title="src/server/actions.js" // ... -export const updateTask = async (args, context) => { +export const updateTask = async ({ id, isDone }, context) => { return context.entities.Task.update({ - where: { id: args.taskId }, + where: { id }, data: { - isDone: args.data.isDone - } + isDone: isDone, + }, }) } ``` -## React logic + + -And we update the React component: -```jsx {2,7-16,23} title="src/client/MainPage.jsx" +```ts title="src/server/actions.ts" +// highlight-next-line +import { CreateTask, UpdateTask } from "@wasp/actions/types" + +// ... + +type UpdateTaskPayload = Pick + +export const updateTask: UpdateTask = async ( + { id, isDone }, + context +) => { + return context.entities.Task.update({ + where: { id }, + data: { + isDone: isDone, + }, + }) +} +``` + + + + +## Invoking the Action on the frontend + +Finally, all that's left to do is call the Action from the React component: + + + + +```jsx {2,7-16,24} title="src/client/MainPage.jsx" // ... import updateTask from '@wasp/actions/updateTask' // ... -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) + } catch (error: any) { + window.alert("Error while updating task: " + error.message) } } return (
- {props.task.description} + {task.description}
) } // ... ``` +
+ + +```tsx {2,4,9-18,26} title="src/client/MainPage.tsx" +// ... +import { FormEvent, ChangeEvent } from "react" +// ... +import updateTask from '@wasp/actions/updateTask' + +// ... + +const Task = ({ task }: { task: Task }) => { + const handleIsDoneChange = async (event: ChangeEvent) => { + try { + await updateTask({ + id: task.id, + isDone: event.target.checked, + }) + } catch (error: any) { + window.alert("Error while updating task: " + error.message) + } + } + + return ( +
+ + {task.description} +
+ ) +} +// ... +``` + +
+
+ Awesome! We can now tick this task as done ;). diff --git a/web/docs/tutorials/todo-app/06-auth.md b/web/docs/tutorials/todo-app/06-auth.md index d8c69e687..7c11926cf 100644 --- a/web/docs/tutorials/todo-app/06-auth.md +++ b/web/docs/tutorials/todo-app/06-auth.md @@ -5,13 +5,13 @@ title: "Authentication" import useBaseUrl from '@docusaurus/useBaseUrl'; -Most of the apps today are multi-user, and Wasp has first-class support for it, so let's see how to add it to our Todo app! +Most of the apps today require some sort of registration and login flows, and Wasp has support for it out of the box, so let's see how to add it to our Todo app! -Let's define a Todo list (luckily we have an app for that now ;)) to get this done: -- [ ] Add Wasp entity `User`. -- [ ] Add `auth` Wasp declaration. -- [ ] Add `Login` and `Signup` pages -- [ ] Modify `src/client/MainPage.jsx` so that it requires authentication. +Let's define a Todo list (luckily we have an app for that now 🙃) to get this done: +- [ ] Add a new entity called `User`. +- [ ] Add `auth` to our `app`. +- [ ] Add `Login` and `Signup` pages. +- [ ] Modify `src/client/MainPage.{jsx,tsx}` so that it requires authentication. - [ ] Add Prisma relation between `User` and `Task` entities. - [ ] Modify our queries and actions so that they work only with the tasks belonging to the authenticated user. - [ ] Add a logout button. @@ -45,10 +45,10 @@ app TodoApp { title: "Todo app", auth: { - // Expects entity User to have (username:String) and (password:String) fields. + // Expects entity User to have username and passwords fields. userEntity: User, methods: { - // We also support Google and GitHub, with more on the way! + // We also support Google, GitHub and email auth, with more on the way! usernameAndPassword: {} }, // We'll see how this is used a bit later @@ -58,9 +58,9 @@ app TodoApp { ``` What this means for us is that Wasp now offers us: - Login and Signup forms located at `@wasp/auth/forms/Login` and `@wasp/auth/forms/Signup` paths, ready to be used. -- `logout()` action. -- React hook `useAuth()`. -- `context.user` as an argument within query/action. +- a `logout()` action. +- a React hook `useAuth()`. +- `context.user` as an argument within queries/actions. This is a very high-level API for auth which makes it very easy to get started quickly, but is not very flexible. If you require more control (e.g. want to execute some custom code on the server @@ -92,6 +92,9 @@ page LoginPage { Great, Wasp now knows how to route these and where to find the pages. Now to the React code of the pages: + + + ```jsx title="src/client/LoginPage.jsx" import { Link } from 'react-router-dom' @@ -112,8 +115,36 @@ const LoginPage = () => { export default LoginPage ``` + + + +```tsx title="src/client/LoginPage.tsx" +import { Link } from 'react-router-dom' + +import { LoginForm } from '@wasp/auth/forms/Login' + +const LoginPage = () => { + return ( + <> + +
+ + I don't have an account yet (go to signup). + + + ) +} + +export default LoginPage +``` +
+
+ The Signup page is very similar to the login one: + + + ```jsx title="src/client/SignupPage.jsx" import { Link } from 'react-router-dom' @@ -133,6 +164,30 @@ const SignupPage = () => { export default SignupPage ``` + + + +```tsx title="src/client/SignupPage.tsx" +import { Link } from 'react-router-dom' + +import { SignupForm } from '@wasp/auth/forms/Signup' + +const SignupPage = () => { + return ( + <> + +
+ + I already have an account (go to login). + + + ) +} + +export default SignupPage +``` +
+
## Updating `MainPage` page to check if the user is authenticated @@ -154,11 +209,26 @@ If an unauthenticated user tries to access route `/` where our page `MainPage` i Also, when `authRequired` is set to `true`, the React component of a page (specified by `component` property within `page`) will be provided `user` object as a prop. It can be accessed like this: + + + ```jsx {1} title="src/client/MainPage.jsx" const MainPage = ({ user }) => { // Do something with the user } ``` + + + +```tsx {3} title="src/client/MainPage.tsx" +import { User } from "@wasp/entities" + +const MainPage = ({ user }: { user: User }) => { + // Do something with the user +} +``` + + Ok, time to try out how this works! @@ -221,18 +291,47 @@ However, for this tutorial, for the sake of simplicity, we will stick with this. ## Updating operations to forbid access to non-authenticated users Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks: -```js {1,4,6} title="src/server/queries.js" + + + + +```js {1,4} title="src/server/queries.js" import HttpError from '@wasp/core/HttpError.js' export const getTasks = async (args, context) => { - if (!context.user) { throw new HttpError(401) } + if (!context.user) { + throw new HttpError(401) + } return context.entities.Task.findMany( { where: { user: { id: context.user.id } } } ) } ``` + + -```js {1,4,8,14,15,16} title="src/server/actions.js" +```ts {3,6} title="src/server/queries.ts" +import { Task } from "@wasp/entities" +import { GetTasks } from "@wasp/queries/types" +import HttpError from '@wasp/core/HttpError.js' + +export const getTasks: GetTasks = async (args, context) => { + if (!context.user) { + throw new HttpError(401) + } + return context.entities.Task.findMany( + { where: { user: { id: context.user.id } } } + ) +} +``` + + + + + + + +```js {1,4,8,14,17,18} title="src/server/actions.js" import HttpError from '@wasp/core/HttpError.js' export const createTask = async (args, context) => { @@ -246,13 +345,57 @@ export const createTask = async (args, context) => { } 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 } }) } ``` + + + +```ts {3,11,17,28,31,32} title="src/server/actions.ts" +import { Task } from "@wasp/entities" +import { CreateTask, UpdateTask } from "@wasp/actions/types" +import HttpError from '@wasp/core/HttpError.js' + +type CreateTaskPayload = Pick + +export const createTask: CreateTask = 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 + +export const updateTask: UpdateTask = async ( + { id, isDone }, + context +) => { + 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 } + }) +} +``` + + :::note Due to how Prisma works, we had to convert `update` to `updateMany` in `updateTask` action to be able to specify the user id in `where`. @@ -280,6 +423,10 @@ You will see that each user has their own tasks, just as we specified in our cod ## Logout button Last, but not least, let's add the logout functionality: + + + + ```jsx {2,10} title="src/client/MainPage.jsx" // ... import logout from '@wasp/auth/logout' @@ -290,10 +437,30 @@ const MainPage = () => { return (
// ... - +
) } ``` +
+ + +```tsx {2,10} title="src/client/MainPage.tsx" +// ... +import logout from '@wasp/auth/logout' +//... + +const MainPage = () => { + // ... + return ( +
+ // ... + +
+ ) +} +``` +
+
This is it, we have a working authentication system, and our Todo app is multi-user! diff --git a/web/docs/tutorials/todo-app/07-dependencies.md b/web/docs/tutorials/todo-app/07-dependencies.md index 27e3db7d9..31586a1a9 100644 --- a/web/docs/tutorials/todo-app/07-dependencies.md +++ b/web/docs/tutorials/todo-app/07-dependencies.md @@ -4,6 +4,7 @@ title: "Dependencies" --- import useBaseUrl from '@docusaurus/useBaseUrl'; +import { ShowForTs } from '@site/src/components/TsJsHelpers'; What is a Todo app without some clocks!? Well, still a Todo app, but certainly not as fun as one with clocks! @@ -15,7 +16,7 @@ app TodoApp { // ... dependencies: [ - ("react-clock", "3.0.0") + ("react-clock", "4.2.0") ] } ``` @@ -27,6 +28,9 @@ wasp start to have Wasp download and install the new dependency. If `wasp start` is already running, Wasp will detect the dependency change, and restart automatically. Next, let's create a new component `Clocks` where we can play with the clocks. + + + ```jsx title="src/client/Clocks.jsx" import { useEffect, useState } from 'react' import Clock from 'react-clock' @@ -50,8 +54,40 @@ const Clocks = () => { export default Clocks ``` + + + +```tsx title="src/client/Clocks.tsx" +import { useEffect, useState } from 'react' +import Clock from 'react-clock' +import 'react-clock/dist/Clock.css' + +const Clocks = () => { + const [time, setTime] = useState(new Date()) + + useEffect(() => { + const interval = setInterval(() => setTime(new Date()), 1000) + return () => clearInterval(interval) + }, []) + + return ( +
+ + +
+ ) +} + +export default Clocks +``` +
+
+ +And let's import it into our `MainPage.{jsx,tsx}` component. + + + -And let's import it into our main React component. ```jsx {2,11} title="src/client/MainPage.jsx" // ... import Clocks from './Clocks' @@ -63,7 +99,7 @@ const MainPage = () => {
// ... -
+
// ...
@@ -71,6 +107,36 @@ const MainPage = () => { } // ... ``` -As you can see, importing other files from `src/client` is completely normal, just use the relative path. The same goes for all files under `src/server`. You can't (and shouldn't) import files from `src/client` into `src/server` and vice versa. If you want to share code between the two runtimes, you can use a relative import to import anything from `src/shared` into both the client code and the server code. The `src/shared` is especially handy for full-stack type definitions when using TypeScript. +
+ + +```tsx {2,11} title="src/client/MainPage.tsx" +// ... +import Clocks from './Clocks' + +const MainPage = () => { + // ... + + return ( +
+ // ... + +
+ + // ... +
+ ) +} +// ... +``` +
+
+ +As you can see, importing other files from `src/client` is completely normal, just use the relative path. The same goes for all files under `src/server`. You can't (and shouldn't) import files from `src/client` into `src/server` and vice versa. If you want to share code between the two runtimes, you can use a relative import to import anything from `src/shared` into both the client code and the server code. + + + +The `src/shared` is especially handy for full-stack type definitions when using TypeScript. + That is it! We added a dependency and used it in our project. diff --git a/web/docs/tutorials/todo-app/08-the-end.md b/web/docs/tutorials/todo-app/08-the-end.md index 3baaf09dc..4be63e878 100644 --- a/web/docs/tutorials/todo-app/08-the-end.md +++ b/web/docs/tutorials/todo-app/08-the-end.md @@ -18,16 +18,16 @@ If you are interested in what is Wasp actually generating in the background, you ## Tailwind CSS Support -We introduced Tailwind CSS support in our Beta version 0.7.0! 🎨 +Wasp supports Tailwind out of the box! 🎨 If you want to style your app with sweet Tailwind CSS classes, all you have to do is add a couple of config files to your app. -If you want to style your app with sweet tailwind CSS classes, all you have to do is add a couple of config files to your app. Check out [this guide](https://www.wasp-lang.dev/blog/2022/11/16/tailwind-feature-announcement) to learn more! +Check out [this guide](/docs/integrations/css-frameworks#tailwind) to learn more! -## Where next? +## What's next? -Well, you could check [the "Language" section](language/overview.md) of the docs for more details on specific parts of Wasp. -Or, you could use Wasp to build something of your own! +Well, you could check the **Guides** on the left of the docs for more details on specific parts of Wasp. Let your imagination run wild and build something of your own! ✨ Maybe something real-time with [WebSockets](/docs/guides/websockets) or with [async jobs](/docs/language/features#jobs)? + +If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next 🙏 -If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next. Even better, if you would like to contribute or help building the feature, let us know! You can find more details on contributing [here](contributing.md). diff --git a/web/docusaurus.config.js b/web/docusaurus.config.js index 43a6f2d51..abc60e4d8 100644 --- a/web/docusaurus.config.js +++ b/web/docusaurus.config.js @@ -1,5 +1,8 @@ const lightCodeTheme = require("prism-react-renderer/themes/github"); +const autoImportTabs = require("./src/remark/auto-import-tabs"); +const fileExtSwitcher = require("./src/remark/file-ext-switcher"); + /** @type {import('@docusaurus/types').DocusaurusConfig} */ module.exports = { title: "Wasp", @@ -151,6 +154,7 @@ module.exports = { sidebarCollapsible: true, // Please change this to your repo. editUrl: "https://github.com/wasp-lang/wasp/edit/main/web", + remarkPlugins: [autoImportTabs, fileExtSwitcher], }, blog: { showReadingTime: true, diff --git a/web/package.json b/web/package.json index c80d6b02f..8c0a71256 100644 --- a/web/package.json +++ b/web/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "start": "docusaurus start", + "start": "docusaurus start --port 3002", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/web/sidebars.js b/web/sidebars.js index 8e66b1599..cc4a2d3b7 100644 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -11,32 +11,25 @@ module.exports = { }, { type: "category", - label: "Tutorials", - collapsed: false, + label: "Tutorial", + collapsed: true, items: [ + "tutorials/todo-app", { type: "category", - label: "Todo app", + label: "Basics", collapsed: true, items: [ - "tutorials/todo-app", - { - type: "category", - label: "Basics", - collapsed: true, - items: [ - "tutorials/todo-app/01-creating-new-project", - "tutorials/todo-app/02-task-entity", - "tutorials/todo-app/03-listing-tasks", - "tutorials/todo-app/04-creating-tasks", - "tutorials/todo-app/05-updating-tasks", - ], - }, - "tutorials/todo-app/06-auth", - "tutorials/todo-app/07-dependencies", - "tutorials/todo-app/08-the-end", + "tutorials/todo-app/01-creating-new-project", + "tutorials/todo-app/02-task-entity", + "tutorials/todo-app/03-listing-tasks", + "tutorials/todo-app/04-creating-tasks", + "tutorials/todo-app/05-updating-tasks", ], }, + "tutorials/todo-app/06-auth", + "tutorials/todo-app/07-dependencies", + "tutorials/todo-app/08-the-end", ], }, "examples", diff --git a/web/src/components/FileExtSwitcher.tsx b/web/src/components/FileExtSwitcher.tsx new file mode 100644 index 000000000..13a10902a --- /dev/null +++ b/web/src/components/FileExtSwitcher.tsx @@ -0,0 +1,26 @@ +// Copied from +// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/components/FileExtSwitcher.tsx + +import * as React from 'react' + +import { useStorageSlot } from '@docusaurus/theme-common' + +interface Props { + path: string +} + +/** + * Takes a path on the form web/src/layouts/BlogLayout/BlogLayout.{js,tsx} and + * replaces the end part, {js,tsx}, with the correct file extension depending + * on what language the user has selected for the code blocks + */ +export default function FileExtSwitcher({ path }: Props) { + const [jsTs] = useStorageSlot('docusaurus.tab.js-ts') + + const extensionStart = path.lastIndexOf('{') + const extensions = path.slice(extensionStart + 1, path.length - 1) + const [jsExt, tsExt] = extensions.split(',') + const pathWithoutExt = path.slice(0, extensionStart) + + return {pathWithoutExt + (jsTs === 'js' ? jsExt : tsExt)} +} diff --git a/web/src/components/TsJsHelpers.tsx b/web/src/components/TsJsHelpers.tsx new file mode 100644 index 000000000..1b6251c92 --- /dev/null +++ b/web/src/components/TsJsHelpers.tsx @@ -0,0 +1,43 @@ +// Copied from (and adapted) +// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/components/ShowForTs.tsx + +import * as React from 'react' + +import { useStorageSlot } from '@docusaurus/theme-common' +import MDXContent from '@theme/MDXContent' + +interface Props { + children: React.ReactNode +} + +/** + * Only renders this block if user has selected TS in the codeblocks + * @Note leave a blank space after opening the tag e.g. + * + * @example + * + * // {blank space} + * ### Mdx Formatted content + * + * **/ +export function ShowForTs({ children }: Props) { + const [jsTs] = useStorageSlot('docusaurus.tab.js-ts') + return jsTs === 'ts' && {children} +} + + +/** + * Only renders this block if user has selected JS in the codeblocks + * @Note leave a blank space after opening the tag e.g. + * + * @example + * + * // {blank space} + * ### Mdx Formatted content + * + * **/ +export function ShowForJs({ children }: Props) { + const [jsTs] = useStorageSlot('docusaurus.tab.js-ts') + return jsTs === 'js' && {children} +} + diff --git a/web/src/remark/auto-import-tabs.js b/web/src/remark/auto-import-tabs.js new file mode 100644 index 000000000..3bdeaa8bb --- /dev/null +++ b/web/src/remark/auto-import-tabs.js @@ -0,0 +1,23 @@ +// Copied from +// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/remark/auto-import-tabs.js + +const needImports = (tree) => + tree.children.some( + (child) => child.type === 'jsx' && /^ (tree, _file) => { + if (needImports(tree)) { + // Add `import` nodes to the top of the parsed file + tree.children.unshift({ + type: 'import', + value: "import Tabs from '@theme/Tabs'", + }) + tree.children.unshift({ + type: 'import', + value: "import TabItem from '@theme/TabItem'", + }) + } +} + +module.exports = plugin diff --git a/web/src/remark/file-ext-switcher.js b/web/src/remark/file-ext-switcher.js new file mode 100644 index 000000000..ec5ef9779 --- /dev/null +++ b/web/src/remark/file-ext-switcher.js @@ -0,0 +1,28 @@ +// Copied from +// https://github.com/redwoodjs/redwood/blob/bd903c5755925ea7174775a2fdaba371b700c910/docs/src/remark/file-ext-switcher.js + +const visit = require('unist-util-visit') + +const plugin = () => { + let needImport = false + + return (tree, _file) => { + visit(tree, (node) => { + if (node.type === 'inlineCode' && /\w\.\{jsx?,tsx?}$/.test(node.value)) { + needImport = true + node.type = 'jsx' + node.value = `` + } + }) + + if (needImport) { + tree.children.unshift({ + type: 'import', + value: + "import FileExtSwitcher from '@site/src/components/FileExtSwitcher'", + }) + } + } +} + +module.exports = plugin