├── basic ├── .prettierignore ├── .prettierrc ├── public │ └── favicon.ico ├── postcss.config.cjs ├── vite.config.ts ├── src │ ├── tags │ │ ├── components │ │ │ ├── colors.ts │ │ │ ├── ColorRadioButton.tsx │ │ │ ├── TagLabel.tsx │ │ │ ├── CreateTagDialog.tsx │ │ │ ├── ColorRadioButtons.tsx │ │ │ └── CreateTagForm.tsx │ │ ├── queries.ts │ │ └── actions.ts │ ├── auth │ │ ├── email │ │ │ ├── RequestPasswordResetPage.tsx │ │ │ ├── userSignupFields.ts │ │ │ ├── PasswordResetPage.tsx │ │ │ ├── EmailVerificationPage.tsx │ │ │ ├── LoginPage.tsx │ │ │ └── SignupPage.tsx │ │ └── AuthLayout.tsx │ ├── App.css │ ├── App.tsx │ ├── vite-env.d.ts │ ├── tasks │ │ ├── queries.ts │ │ ├── TasksPage.tsx │ │ ├── actions.ts │ │ └── components │ │ │ ├── TaskListItem.tsx │ │ │ ├── TaskList.tsx │ │ │ └── CreateTaskForm.tsx │ ├── shared │ │ └── components │ │ │ ├── Portal.tsx │ │ │ ├── Input.tsx │ │ │ ├── Header.tsx │ │ │ ├── Button.tsx │ │ │ └── Dialog.tsx │ └── assets │ │ └── logo.svg ├── eslint.config.js ├── package.json ├── tailwind.config.cjs ├── schema.prisma ├── README.md └── main.wasp ├── minimal ├── public │ └── favicon.ico ├── vite.config.ts ├── schema.prisma ├── main.wasp ├── package.json └── src │ ├── vite-env.d.ts │ ├── MainPage.tsx │ ├── assets │ └── logo.svg │ └── Main.css ├── .gitignore ├── LICENSE └── README.md /basic/.prettierignore: -------------------------------------------------------------------------------- 1 | .wasp 2 | node_modules 3 | -------------------------------------------------------------------------------- /basic/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasp-lang/starters/HEAD/basic/public/favicon.ico -------------------------------------------------------------------------------- /minimal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasp-lang/starters/HEAD/minimal/public/favicon.ico -------------------------------------------------------------------------------- /basic/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | server: { 5 | open: true, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /minimal/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | server: { 5 | open: true, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /basic/src/tags/components/colors.ts: -------------------------------------------------------------------------------- 1 | export function generateBrightColor( 2 | hue = Math.floor(Math.random() * 360), 3 | ): string { 4 | const saturation = 90; 5 | const lightness = 80; 6 | 7 | return `hsl(${hue}, ${saturation}%, ${lightness}%)`; 8 | } 9 | -------------------------------------------------------------------------------- /basic/src/auth/email/RequestPasswordResetPage.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from "wasp/client/auth"; 2 | import { AuthLayout } from "../AuthLayout"; 3 | 4 | export function RequestPasswordResetPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /basic/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .card { 7 | @apply rounded-xl border border-neutral-200 bg-white shadow-sm transition-all duration-200; 8 | } 9 | 10 | .label { 11 | @apply text-sm font-medium text-neutral-700; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /minimal/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | // Wasp requires that the url is set to the DATABASE_URL environment variable. 4 | url = env("DATABASE_URL") 5 | } 6 | 7 | // Wasp requires the `prisma-client-js` generator to be present. 8 | generator client { 9 | provider = "prisma-client-js" 10 | } 11 | -------------------------------------------------------------------------------- /minimal/main.wasp: -------------------------------------------------------------------------------- 1 | app __waspAppName__ { 2 | wasp: { 3 | version: "__waspVersion__" 4 | }, 5 | title: "__waspProjectName__", 6 | head: [ 7 | "", 8 | ] 9 | } 10 | 11 | route RootRoute { path: "/", to: MainPage } 12 | page MainPage { 13 | component: import { MainPage } from "@src/MainPage" 14 | } 15 | -------------------------------------------------------------------------------- /basic/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import "./App.css"; 3 | import { Header } from "./shared/components/Header"; 4 | 5 | export function App() { 6 | return ( 7 |
8 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /basic/src/auth/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | export function AuthLayout({ children }: React.PropsWithChildren) { 2 | return ( 3 |
4 | {/* Auth UI has margin-top on title, so we lower the top padding */} 5 |
6 | {children} 7 |
8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__waspAppName__", 3 | "type": "module", 4 | "dependencies": { 5 | "wasp": "file:.wasp/out/sdk/wasp", 6 | "react": "^18.2.0", 7 | "react-dom": "^18.2.0", 8 | "react-router-dom": "^6.26.2" 9 | }, 10 | "devDependencies": { 11 | "typescript": "^5.1.0", 12 | "vite": "^4.3.9", 13 | "@types/react": "^18.0.37", 14 | "prisma": "5.19.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // This is needed to properly support Vitest testing with jest-dom matchers. 4 | // Types for jest-dom are not recognized automatically and Typescript complains 5 | // about missing types e.g. when using `toBeInTheDocument` and other matchers. 6 | // Reference: https://github.com/testing-library/jest-dom/issues/546#issuecomment-1889884843 7 | import "@testing-library/jest-dom"; 8 | -------------------------------------------------------------------------------- /minimal/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // This is needed to properly support Vitest testing with jest-dom matchers. 4 | // Types for jest-dom are not recognized automatically and Typescript complains 5 | // about missing types e.g. when using `toBeInTheDocument` and other matchers. 6 | // Reference: https://github.com/testing-library/jest-dom/issues/546#issuecomment-1889884843 7 | import "@testing-library/jest-dom"; 8 | -------------------------------------------------------------------------------- /basic/src/tags/queries.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "wasp/entities"; 2 | import { HttpError } from "wasp/server"; 3 | import { type GetTags } from "wasp/server/operations"; 4 | 5 | export const getTags: GetTags = (_, context) => { 6 | if (!context.user) { 7 | throw new HttpError(401); 8 | } 9 | 10 | return context.entities.Tag.findMany({ 11 | where: { user: { id: context.user.id } }, 12 | orderBy: { name: "asc" }, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /basic/src/auth/email/userSignupFields.ts: -------------------------------------------------------------------------------- 1 | import { defineUserSignupFields } from "wasp/server/auth"; 2 | 3 | export const userSignupFields = defineUserSignupFields({ 4 | username: (data) => { 5 | if (typeof data.username !== "string") { 6 | throw new Error("Username is required."); 7 | } 8 | if (data.username.length < 6) { 9 | throw new Error("Username must be at least 6 characters long."); 10 | } 11 | return data.username; 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /basic/src/auth/email/PasswordResetPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { ResetPasswordForm } from "wasp/client/auth"; 3 | import { AuthLayout } from "../AuthLayout"; 4 | 5 | export function PasswordResetPage() { 6 | return ( 7 | 8 | 9 |
10 | 11 | {"If everything is okay, "} 12 | 13 | go to login 14 | 15 | . 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /basic/src/auth/email/EmailVerificationPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { VerifyEmailForm } from "wasp/client/auth"; 3 | import { AuthLayout } from "../AuthLayout"; 4 | 5 | export function EmailVerificationPage() { 6 | return ( 7 | 8 | 9 |
10 | 11 | {"If everything is okay, "} 12 | 13 | go to login 14 | 15 | . 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files. 2 | .wasp/ 3 | node_modules/ 4 | package-lock.json 5 | 6 | # Since we don't want to lock in users into a specific database. 7 | migrations/ 8 | 9 | # MacOS specific files. 10 | .DS_Store 11 | 12 | # Files handled by wasp template skeleton. 13 | */.gitignore 14 | .waspignore 15 | .wasproot 16 | tsconfig.json 17 | 18 | # Ignore all dotenv files by default to prevent accidentally committing any secrets. 19 | # To include specific dotenv files, use the `!` operator or adjust these rules. 20 | .env 21 | .env.* 22 | 23 | # Don't ignore example dotenv files. 24 | !.env.example 25 | !.env.*.example 26 | -------------------------------------------------------------------------------- /basic/src/tags/actions.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "wasp/entities"; 2 | import { HttpError } from "wasp/server"; 3 | import { CreateTag } from "wasp/server/operations"; 4 | 5 | type CreateTagArgs = Pick; 6 | 7 | export const createTag: CreateTag = (tag, context) => { 8 | if (!context.user) { 9 | throw new HttpError(401); 10 | } 11 | 12 | return context.entities.Tag.create({ 13 | data: { 14 | name: tag.name, 15 | color: tag.color, 16 | user: { 17 | connect: { 18 | id: context.user.id, 19 | }, 20 | }, 21 | }, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /basic/src/tasks/queries.ts: -------------------------------------------------------------------------------- 1 | import { Tag, Task } from "wasp/entities"; 2 | import { HttpError } from "wasp/server"; 3 | import { type GetTasks } from "wasp/server/operations"; 4 | 5 | export type TaskWithTags = Task & { tags: Tag[] }; 6 | 7 | export const getTasks: GetTasks = (_args, context) => { 8 | if (!context.user) { 9 | throw new HttpError(401); 10 | } 11 | 12 | return context.entities.Task.findMany({ 13 | where: { user: { id: context.user.id } }, 14 | orderBy: { id: "asc" }, 15 | include: { 16 | tags: { 17 | orderBy: { name: "asc" }, 18 | }, 19 | }, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /basic/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintPrettier from "eslint-config-prettier/flat"; 3 | import eslintReact from "eslint-plugin-react"; 4 | import { defineConfig } from "eslint/config"; 5 | import globals from "globals"; 6 | import eslintTypescript from "typescript-eslint"; 7 | 8 | export default defineConfig([ 9 | { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"] }, 10 | { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], languageOptions: { globals: { ...globals.browser, ...globals.node } } }, 11 | eslintTypescript.configs.recommended, 12 | eslintReact.configs.flat.recommended, 13 | eslintReact.configs.flat['jsx-runtime'], 14 | eslintPrettier, 15 | ]); 16 | -------------------------------------------------------------------------------- /basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "type": "module", 4 | "dependencies": { 5 | "react": "^18.2.0", 6 | "react-dom": "^18.2.0", 7 | "react-hook-form": "^7.57.0", 8 | "react-router-dom": "^6.26.2", 9 | "tailwind-merge": "~2.6.0", 10 | "wasp": "file:.wasp/out/sdk/wasp" 11 | }, 12 | "devDependencies": { 13 | "@eslint/js": "^9.27.0", 14 | "@types/react": "^18.0.37", 15 | "eslint": "^9.27.0", 16 | "eslint-config-prettier": "^10.1.5", 17 | "eslint-plugin-react": "^7.37.5", 18 | "globals": "^16.1.0", 19 | "prettier": "^3.5.3", 20 | "prettier-plugin-tailwindcss": "^0.6.11", 21 | "prisma": "5.19.1", 22 | "tailwindcss": "^3.4.17", 23 | "typescript": "^5.1.0", 24 | "typescript-eslint": "^8.32.1", 25 | "vite": "^4.3.9" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /basic/src/tasks/TasksPage.tsx: -------------------------------------------------------------------------------- 1 | import { type AuthUser } from "wasp/auth"; 2 | import { CreateTaskForm } from "./components/CreateTaskForm"; 3 | import { TaskList } from "./components/TaskList"; 4 | 5 | export const TasksPage = ({ user }: { user: AuthUser }) => { 6 | return ( 7 |
8 |

{`${user.username}'s tasks`}

9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /basic/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | import { resolveProjectPath } from "wasp/dev"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [resolveProjectPath("./src/**/*.{js,jsx,ts,tsx}")], 6 | theme: { 7 | extend: { 8 | fontSize: { 9 | "tiny": ["0.625rem", "1rem"], // 10px 10 | }, 11 | colors: { 12 | // Created using https://www.tints.dev 13 | primary: { 14 | 50: "#FFFBEB", 15 | 100: "#FFF7D6", 16 | 200: "#FFEFAD", 17 | 300: "#FFE680", 18 | 400: "#FFDA47", 19 | 500: "#FFCC00", 20 | 600: "#E6B800", 21 | 700: "#CCA300", 22 | 800: "#A88700", 23 | 900: "#7A6200", 24 | 950: "#574500" 25 | }, 26 | }, 27 | }, 28 | }, 29 | plugins: [], 30 | }; 31 | -------------------------------------------------------------------------------- /basic/src/auth/email/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { LoginForm } from "wasp/client/auth"; 3 | import { AuthLayout } from "../AuthLayout"; 4 | 5 | export function LoginPage() { 6 | return ( 7 | 8 | 9 |
10 | 11 | {"Don't have an account yet? "} 12 | 13 | Go to signup 14 | 15 | . 16 | 17 |
18 | 19 | {"Forgot your password? "} 20 | 21 | Reset it 22 | 23 | . 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /basic/src/shared/components/Portal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | interface PortalProps extends React.HTMLAttributes { 5 | container?: Element | null; 6 | } 7 | 8 | /** 9 | * Inspired by Radix UI's Portal component. 10 | * @see https://github.com/radix-ui/primitives/blob/main/packages/react/portal/src/portal.tsx 11 | */ 12 | export const Portal = React.forwardRef( 13 | function Portal({ container: containerProp, ...portalProps }, forwardRef) { 14 | const [mounted, setMounted] = React.useState(false); 15 | React.useLayoutEffect(() => setMounted(true), []); 16 | 17 | const container = containerProp || (mounted && globalThis?.document?.body); 18 | 19 | return container 20 | ? ReactDOM.createPortal( 21 |
, 22 | container, 23 | ) 24 | : null; 25 | }, 26 | ); 27 | -------------------------------------------------------------------------------- /basic/src/auth/email/SignupPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { SignupForm } from "wasp/client/auth"; 3 | import { AuthLayout } from "../AuthLayout"; 4 | 5 | export function SignupPage() { 6 | return ( 7 | 8 | 24 |
25 | 26 | {"Already have an account? "} 27 | 28 | Go to login 29 | 30 | . 31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /minimal/src/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import Logo from "./assets/logo.svg"; 2 | import "./Main.css"; 3 | 4 | export function MainPage() { 5 | return ( 6 |
7 | wasp 8 | 9 |

Welcome to Wasp!

10 | 11 |

12 | This is page MainPage located at route /. 13 |
14 | Open src/MainPage.tsx to edit it. 15 |

16 | 17 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /basic/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | // Use Prisma Schema file to define your entities: https://www.prisma.io/docs/concepts/components/prisma-schema. 11 | // Run `wasp db migrate-dev` in the CLI to create the database tables, 12 | // then run `wasp db studio` to open Prisma Studio and view your db models. 13 | model User { 14 | id String @id @default(uuid()) 15 | username String 16 | 17 | tasks Task[] 18 | tags Tag[] 19 | } 20 | 21 | model Task { 22 | id String @id @default(uuid()) 23 | description String 24 | isDone Boolean @default(false) 25 | createdAt DateTime @default(now()) 26 | 27 | user User @relation(fields: [userId], references: [id]) 28 | userId String 29 | 30 | tags Tag[] 31 | } 32 | 33 | model Tag { 34 | id String @id @default(uuid()) 35 | name String @unique 36 | color String 37 | 38 | user User @relation(fields: [userId], references: [id]) 39 | userId String 40 | 41 | tasks Task[] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 wasp-lang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /basic/src/tags/components/ColorRadioButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { twJoin } from "tailwind-merge"; 3 | 4 | interface ColorRadioButtonProps 5 | extends Required< 6 | Pick< 7 | React.InputHTMLAttributes, 8 | "name" | "checked" | "value" | "onChange" | "title" 9 | > 10 | > { 11 | bgColor: string; 12 | } 13 | 14 | export function ColorRadioButton({ 15 | checked, 16 | bgColor, 17 | title, 18 | ...props 19 | }: ColorRadioButtonProps) { 20 | const id = React.useId(); 21 | 22 | return ( 23 |
24 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /basic/src/shared/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ControllerFieldState } from "react-hook-form"; 3 | import { twJoin } from "tailwind-merge"; 4 | 5 | interface InputProps 6 | extends Omit, "children" | "id"> { 7 | label: string; 8 | fieldState: ControllerFieldState; 9 | } 10 | 11 | export const Input = React.forwardRef( 12 | function Input({ className, label, fieldState, ...props }, ref) { 13 | const id = React.useId(); 14 | return ( 15 |
16 | 19 | 28 | {fieldState.error && ( 29 | 30 | {fieldState.error.message} 31 | 32 | )} 33 |
34 | ); 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic Starter – A Simple ToDo App 2 | 3 | Basic starter is a well-rounded template that showcases the most important bits of working with Wasp. 4 | 5 | ## Prerequisites 6 | 7 | - **Node.js** (newest LTS version recommended): We recommend install Node through a Node version manager, e.g. `nvm`. 8 | - **Wasp** (latest version): Install via 9 | ```sh 10 | curl -sSL https://get.wasp.sh/installer.sh | sh 11 | ``` 12 | 13 | ## Using the template 14 | 15 | You can use this template through the Wasp CLI: 16 | 17 | ```bash 18 | wasp new 19 | # or 20 | wasp new -t basic 21 | ``` 22 | 23 | ## Development 24 | 25 | To start the application locally for development or preview purposes: 26 | 27 | 1. Run `wasp db migrate-dev` to migrate the database to the latest migration 28 | 2. Run `wasp start` to start the Wasp application. If running for the first time, this will also install the client and the server dependencies for you. 29 | 3. The application should be running on `localhost:3000`. Open in it your browser to access the client. 30 | 31 | To improve your Wasp development experience, we recommend installing the [Wasp extension for VSCode](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp). 32 | 33 | ## Learn more 34 | 35 | To find out more about Wasp, visit out [docs](https://wasp.sh/docs). 36 | -------------------------------------------------------------------------------- /basic/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /minimal/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /basic/src/shared/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { logout, useAuth } from "wasp/client/auth"; 2 | import { Link } from "wasp/client/router"; 3 | import Logo from "../../assets/logo.svg"; 4 | import { Button, ButtonLink } from "./Button"; 5 | 6 | export function Header() { 7 | const { data: user } = useAuth(); 8 | 9 | return ( 10 |
11 |
12 | 13 | Todo App Logo 14 |

Todo App

15 | 16 | 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /basic/src/tags/components/TagLabel.tsx: -------------------------------------------------------------------------------- 1 | import { ClassNameValue, twJoin } from "tailwind-merge"; 2 | import { Tag } from "wasp/entities"; 3 | 4 | type TagLabelSize = "md" | "sm" | "tiny"; 5 | 6 | interface TagLabelProps { 7 | tag: Pick; 8 | isActive: boolean; 9 | size?: TagLabelSize; 10 | showColorCircle?: boolean; 11 | } 12 | 13 | export function TagLabel({ 14 | tag, 15 | isActive, 16 | size = "md", 17 | showColorCircle = false, 18 | }: TagLabelProps) { 19 | return ( 20 | 29 | {tag.name} 30 | {showColorCircle && ( 31 | 40 | )} 41 | 42 | ); 43 | } 44 | 45 | const sizeStyles: Record = { 46 | md: "px-4 py-1.5 text-sm", 47 | sm: "px-3 py-1 text-xs", 48 | tiny: "px-2 py-0.5 text-xs", 49 | }; 50 | 51 | const colorCircleSizeStyles: Record = { 52 | md: "h-3 w-3 -right-2", 53 | sm: "h-2 w-2 -right-1", 54 | tiny: "h-1.5 w-1.5 -right-0.5", 55 | }; 56 | -------------------------------------------------------------------------------- /basic/src/tasks/actions.ts: -------------------------------------------------------------------------------- 1 | import { type Tag, type Task } from "wasp/entities"; 2 | import { HttpError } from "wasp/server"; 3 | import { 4 | DeleteCompletedTasks, 5 | type CreateTask, 6 | type UpdateTaskStatus, 7 | } from "wasp/server/operations"; 8 | 9 | type CreateTaskArgs = Pick & { 10 | tagIds: Tag["id"][]; 11 | }; 12 | 13 | export const createTask: CreateTask = async ( 14 | { description, tagIds }, 15 | context, 16 | ) => { 17 | if (!context.user) { 18 | throw new HttpError(401); 19 | } 20 | 21 | return context.entities.Task.create({ 22 | data: { 23 | description, 24 | isDone: false, 25 | user: { 26 | connect: { 27 | id: context.user.id, 28 | }, 29 | }, 30 | tags: { 31 | connect: tagIds.map((tag) => ({ 32 | id: tag, 33 | })), 34 | }, 35 | }, 36 | }); 37 | }; 38 | 39 | type UpdateTaskStatusArgs = Pick; 40 | 41 | export const updateTaskStatus: UpdateTaskStatus = async ( 42 | { id, isDone }, 43 | context, 44 | ) => { 45 | if (!context.user) { 46 | throw new HttpError(401); 47 | } 48 | 49 | return context.entities.Task.update({ 50 | where: { 51 | id, 52 | }, 53 | data: { isDone }, 54 | }); 55 | }; 56 | 57 | export const deleteCompletedTasks: DeleteCompletedTasks = async ( 58 | _args, 59 | context, 60 | ) => { 61 | if (!context.user) { 62 | throw new HttpError(401); 63 | } 64 | 65 | return context.entities.Task.deleteMany({ 66 | where: { 67 | userId: context.user.id, 68 | isDone: true, 69 | }, 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /basic/src/tasks/components/TaskListItem.tsx: -------------------------------------------------------------------------------- 1 | import { twJoin } from "tailwind-merge"; 2 | import { updateTaskStatus } from "wasp/client/operations"; 3 | import { TagLabel } from "../../tags/components/TagLabel"; 4 | import { TaskWithTags } from "../queries"; 5 | 6 | interface TaskListItemProps { 7 | task: TaskWithTags; 8 | } 9 | 10 | export function TaskListItem({ task }: TaskListItemProps) { 11 | async function setTaskDone( 12 | event: React.ChangeEvent, 13 | ): Promise { 14 | try { 15 | await updateTaskStatus({ 16 | id: task.id, 17 | isDone: event.currentTarget.checked, 18 | }); 19 | } catch (err: unknown) { 20 | window.alert(`Error while updating task: ${String(err)}`); 21 | } 22 | } 23 | 24 | return ( 25 |
  • 26 | 54 |
  • 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /basic/src/tags/components/CreateTagDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "../../shared/components/Button"; 3 | import { Dialog } from "../../shared/components/Dialog"; 4 | import { Portal } from "../../shared/components/Portal"; 5 | import { CREATE_TAG_FORM_ID, CreateTagForm } from "./CreateTagForm"; 6 | 7 | export function CreateTagDialog() { 8 | const [tagDialogOpen, setTagDialogOpen] = React.useState(false); 9 | 10 | return ( 11 | <> 12 | 21 | {tagDialogOpen && ( 22 | 23 | setTagDialogOpen(false)}> 24 |
    25 |
    26 |

    Create a new tag

    27 |
    28 |
    29 | setTagDialogOpen(false)} /> 30 |
    31 |
    32 | 35 | 43 |
    44 |
    45 |
    46 |
    47 | )} 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /basic/src/tags/components/ColorRadioButtons.tsx: -------------------------------------------------------------------------------- 1 | import { ColorRadioButton } from "./ColorRadioButton"; 2 | import { generateBrightColor } from "./colors"; 3 | 4 | interface ColorRadioButtonsProps { 5 | color: string; 6 | setColor: (color: string) => void; 7 | } 8 | 9 | export function ColorRadioButtons({ color, setColor }: ColorRadioButtonsProps) { 10 | const randomColor = generateBrightColor(); 11 | 12 | return ( 13 |
    14 | Color 15 |
    16 | setColor(randomColor)} 21 | title="Random" 22 | bgColor={`conic-gradient( 23 | hsl(360 100% 50%), 24 | hsl(315 100% 50%), 25 | hsl(270 100% 50%), 26 | hsl(225 100% 50%), 27 | hsl(180 100% 50%), 28 | hsl(135 100% 50%), 29 | hsl(90 100% 50%), 30 | hsl(45 100% 50%), 31 | hsl(0 100% 50%) 32 | )`} 33 | /> 34 | {staticColors.map((staticColor, index) => ( 35 | setColor(staticColor)} 41 | title={`Color ${index + 1}`} 42 | bgColor={staticColor} 43 | /> 44 | ))} 45 |
    46 |
    47 | ); 48 | } 49 | 50 | const staticColors = generateBrightColors(); 51 | 52 | function generateBrightColors(): string[] { 53 | const colors: string[] = []; 54 | for (let hue = 0; hue < 360; hue += 20) { 55 | const hslColor = generateBrightColor(hue); 56 | colors.push(hslColor); 57 | } 58 | return colors; 59 | } 60 | -------------------------------------------------------------------------------- /minimal/src/Main.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 11 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 12 | sans-serif; 13 | } 14 | 15 | #root { 16 | min-height: 100vh; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | 23 | .container { 24 | margin: 3rem 3rem 10rem 3rem; 25 | max-width: 726px; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | text-align: center; 31 | } 32 | 33 | .logo { 34 | max-height: 200px; 35 | margin-bottom: 1rem; 36 | } 37 | 38 | .title { 39 | font-size: 4rem; 40 | font-weight: 700; 41 | margin-bottom: 1rem; 42 | } 43 | 44 | .content { 45 | font-size: 1.2rem; 46 | font-weight: 400; 47 | line-height: 2; 48 | margin-bottom: 3rem; 49 | } 50 | 51 | .buttons { 52 | display: flex; 53 | flex-direction: row; 54 | gap: 1rem; 55 | } 56 | 57 | .button { 58 | font-size: 1.2rem; 59 | font-weight: 700; 60 | text-decoration: none; 61 | padding: 1.2rem 1.5rem; 62 | border-radius: 10px; 63 | } 64 | 65 | .button-filled { 66 | color: black; 67 | background-color: #ffcc00; 68 | border: 2px solid #ffcc00; 69 | 70 | transition: all 0.2s ease-in-out; 71 | } 72 | 73 | .button-filled:hover { 74 | filter: brightness(0.95); 75 | } 76 | 77 | .button-outlined { 78 | color: black; 79 | background-color: transparent; 80 | border: 2px solid #ffcc00; 81 | 82 | transition: all 0.2s ease-in-out; 83 | } 84 | 85 | .button-outlined:hover { 86 | filter: brightness(0.95); 87 | } 88 | 89 | code { 90 | border-radius: 5px; 91 | border: 1px solid #ffcc00; 92 | padding: 0.2rem; 93 | background: #ffcc0044; 94 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 95 | Bitstream Vera Sans Mono, Courier New, monospace; 96 | } 97 | -------------------------------------------------------------------------------- /basic/src/tasks/components/TaskList.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | deleteCompletedTasks, 3 | getTasks, 4 | useQuery, 5 | } from "wasp/client/operations"; 6 | import { Button } from "../../shared/components/Button"; 7 | import { TaskListItem } from "./TaskListItem"; 8 | 9 | export function TaskList() { 10 | const { data: tasks, isLoading, isSuccess } = useQuery(getTasks); 11 | 12 | if (isLoading) { 13 | return

    Loading...

    ; 14 | } 15 | 16 | if (!isSuccess) { 17 | return

    Error loading tasks.

    ; 18 | } 19 | 20 | const completedTasks = tasks.filter((task) => task.isDone); 21 | 22 | async function handleDeleteCompletedTasks() { 23 | try { 24 | await deleteCompletedTasks(); 25 | } catch (err: unknown) { 26 | window.alert(`Error while deleting tasks: ${String(err)}`); 27 | } 28 | } 29 | 30 | return ( 31 |
    32 |
      33 | {tasks.map((task) => ( 34 | 35 | ))} 36 |
    37 |
    38 |
    39 | 40 | {tasks.length} {tasks.length === 1 ? "task" : "tasks"} 41 | 42 | · 43 | {completedTasks.length} completed 44 |
    45 | {completedTasks.length > 0 && ( 46 | 60 | )} 61 |
    62 |
    63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /basic/src/shared/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ClassNameValue, twJoin } from "tailwind-merge"; 2 | import { Link } from "wasp/client/router"; 3 | 4 | type ButtonSize = "md" | "sm" | "xs"; 5 | type ButtonVariant = "primary" | "danger" | "ghost"; 6 | 7 | interface ButtonProps extends React.ButtonHTMLAttributes { 8 | size?: ButtonSize; 9 | variant?: ButtonVariant; 10 | } 11 | 12 | export function Button({ 13 | children, 14 | className, 15 | type = "button", 16 | size = "md", 17 | variant = "primary", 18 | ...props 19 | }: ButtonProps) { 20 | return ( 21 | 32 | ); 33 | } 34 | 35 | type ButtonLinkProps = React.ComponentProps & { 36 | size?: ButtonSize; 37 | variant?: ButtonVariant; 38 | }; 39 | 40 | export function ButtonLink({ 41 | children, 42 | className, 43 | size = "md", 44 | variant = "primary", 45 | ...props 46 | }: ButtonLinkProps) { 47 | return ( 48 | 56 | {children} 57 | 58 | ); 59 | } 60 | 61 | function getButtonClasses({ 62 | size, 63 | variant, 64 | className, 65 | }: { 66 | size: ButtonSize; 67 | variant: ButtonVariant; 68 | className: ClassNameValue; 69 | }): string { 70 | return twJoin( 71 | "rounded-md font-semibold", 72 | variantStyles[variant], 73 | sizeStyles[size], 74 | className, 75 | ); 76 | } 77 | 78 | const sizeStyles: Record = { 79 | md: "px-4 py-2", 80 | sm: "px-3 py-1.5 text-sm", 81 | xs: "px-2 py-1 text-xs", 82 | }; 83 | 84 | const variantStyles: Record = { 85 | primary: 86 | "bg-primary-500 hover:bg-primary-400 active:bg-primary-300 text-neutral-800", 87 | danger: "bg-red-600 text-white hover:bg-red-700 active:bg-red-800", 88 | ghost: 89 | "bg-transparent text-neutral-800 hover:bg-neutral-100 active:bg-neutral-200", 90 | }; 91 | -------------------------------------------------------------------------------- /basic/src/shared/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { twJoin } from "tailwind-merge"; 3 | 4 | interface DialogProps extends React.PropsWithChildren { 5 | open: boolean; 6 | onClose: () => void; 7 | closeOnClickOutside?: boolean; 8 | } 9 | 10 | export function Dialog({ 11 | open, 12 | onClose, 13 | children, 14 | closeOnClickOutside = true, 15 | }: DialogProps) { 16 | const dialogRef = React.useRef(null); 17 | 18 | React.useEffect( 19 | function handleShowOrCloseDialog() { 20 | const dialog = dialogRef.current; 21 | if (!dialog) return; 22 | 23 | if (open && !dialog.open) { 24 | dialog.showModal(); 25 | } else if (!open && dialog.open) { 26 | dialog.close(); 27 | } 28 | }, 29 | [open], 30 | ); 31 | 32 | React.useEffect( 33 | function handleCloseOnClickOutside() { 34 | if (!closeOnClickOutside) return; 35 | 36 | const dialog = dialogRef.current; 37 | if (!dialog) return; 38 | 39 | const handleClick = (e: MouseEvent) => { 40 | const rect = dialog.getBoundingClientRect(); 41 | const clickedOutside = 42 | e.clientX < rect.left || 43 | e.clientX > rect.right || 44 | e.clientY < rect.top || 45 | e.clientY > rect.bottom; 46 | 47 | if (clickedOutside) { 48 | onClose(); 49 | } 50 | }; 51 | 52 | dialog.addEventListener("click", handleClick); 53 | return () => { 54 | dialog.removeEventListener("click", handleClick); 55 | }; 56 | }, 57 | [closeOnClickOutside, onClose], 58 | ); 59 | 60 | React.useEffect( 61 | function handlePreventScroll() { 62 | if (!open) return; 63 | 64 | const originalOverflow = document.body.style.overflow; 65 | document.body.style.overflow = "hidden"; 66 | return () => { 67 | document.body.style.overflow = originalOverflow; 68 | }; 69 | }, 70 | [open], 71 | ); 72 | 73 | return ( 74 | 82 | {children} 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This Repository is Deprecated 2 | This repository is archived no longer in use. We've integrated the starter templates into [the Wasp compiler](https://github.com/wasp-lang/wasp/tree/bc827cf46477fc5ddf5a4948787aec4ed244514c/waspc/data/Cli/starters). 3 | 4 | ---- 5 | 6 | # Welcome to Wasp Starters 👋 7 | 8 | In this repository you'll find some of the starters to speed up your initial project with [Wasp Lang](https://wasp.sh/) 9 | 10 | If you don't already have it, you can install Wasp by going [here](https://wasp.sh/docs). 11 | 12 | ## Available starters 13 | 14 | > **Note** After you create a new project, make sure to check the README.md to see any additional info 15 | 16 | ### Minimal 17 | 18 | A minimal Wasp App with a single hello page. 19 | Perfect for minimalists. 20 | 21 | To use this tempalte: 22 | 23 | ```bash 24 | wasp new -t minimal 25 | ``` 26 | 27 | ### Basic 28 | 29 | Basic is a well-rounded tempalte which showcases most important bits of working with Wasp. 30 | It is also the default template. 31 | 32 | To use this tempalte: 33 | 34 | ```bash 35 | wasp new 36 | # or 37 | wasp new -t basic 38 | ``` 39 | 40 | ### SaaS Template 41 | 42 | A SaaS Template to get your profitable side-project started quickly and easily! 43 | 44 | It used to be here, but now it became big enough to have its own repo: check it out at https://github.com/wasp-lang/open-saas . 45 | 46 | ## If you are looking to contribute a template 47 | 48 | Adding a new template includes: 49 | 50 | 1. Create a new folder in the root of the repo and write the Wasp app code in it, for whatever you want your template to be. 51 | 2. Put the placeholders in `main.wasp` instead of the app name and `title`, if you wish (check how other templates do this). 52 | 3. Create a PR! In the PR, ask a core team do add template to the list of templates in the code of Wasp CLI, in https://github.com/wasp-lang/wasp/blob/main/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates.hs . 53 | You could also do this on your own, but it involves Haskell and setting it up might be quite time consuming if you never used it before, so we advise leaving it to the core team. 54 | 55 | ## Core team: tag management 56 | 57 | If we updated templates for the existing `wasp` version, then we also need to update the tag that current `wasp` uses to fetch the templates to point to our latest changes. 58 | 59 | If new major version of `wasp` came out and we want to update the templates so they work with this new version of `wasp`, then we should not touch existing tags but should instead create a new tag that corresponds to the one that new `wasp` expects, and we should make it point to our latest changes. 60 | 61 | Adding a new tag: 62 | 63 | ```bash 64 | git tag wasp-v0.16-template 65 | git push origin wasp-v0.16-template 66 | ``` 67 | 68 | Updating existing tag: 69 | 70 | ```bash 71 | git tag -f wasp-v0.16-template 72 | git push origin -f wasp-v0.16-template 73 | ``` 74 | -------------------------------------------------------------------------------- /basic/main.wasp: -------------------------------------------------------------------------------- 1 | app __waspAppName__ { 2 | wasp: { 3 | version: "__waspVersion__" 4 | }, 5 | title: "__waspProjectName__", 6 | head: [ 7 | "", 8 | ], 9 | auth: { 10 | userEntity: User, 11 | methods: { 12 | email: { 13 | fromField: { 14 | name: "Basic App", 15 | email: "hello@example.com" 16 | }, 17 | userSignupFields: import { userSignupFields } from "@src/auth/email/userSignupFields", 18 | emailVerification: { 19 | clientRoute: EmailVerificationRoute, 20 | }, 21 | passwordReset: { 22 | clientRoute: PasswordResetRoute, 23 | } 24 | }, 25 | }, 26 | onAuthSucceededRedirectTo: "/", 27 | onAuthFailedRedirectTo: "/login", 28 | }, 29 | emailSender: { 30 | provider: Dummy, 31 | }, 32 | client: { 33 | rootComponent: import { App } from "@src/App.tsx", 34 | } 35 | } 36 | 37 | // #region Auth 38 | route LoginRoute { path: "/login", to: LoginPage } 39 | page LoginPage { 40 | component: import { LoginPage } from "@src/auth/email/LoginPage.tsx" 41 | } 42 | 43 | route SignupRoute { path: "/signup", to: SignupPage } 44 | page SignupPage { 45 | component: import { SignupPage } from "@src/auth/email/SignupPage.tsx" 46 | } 47 | 48 | route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } 49 | page RequestPasswordResetPage { 50 | component: import { RequestPasswordResetPage } from "@src/auth/email/RequestPasswordResetPage.tsx", 51 | } 52 | 53 | route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } 54 | page PasswordResetPage { 55 | component: import { PasswordResetPage } from "@src/auth/email/PasswordResetPage.tsx", 56 | } 57 | 58 | route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } 59 | page EmailVerificationPage { 60 | component: import { EmailVerificationPage } from "@src/auth/email/EmailVerificationPage.tsx", 61 | } 62 | // #endregion Auth 63 | 64 | // #region Tasks 65 | route TasksRoute { path: "/", to: TasksPage } 66 | page TasksPage { 67 | authRequired: true, 68 | component: import { TasksPage } from "@src/tasks/TasksPage.tsx" 69 | } 70 | 71 | query getTasks { 72 | fn: import { getTasks } from "@src/tasks/queries", 73 | entities: [Task, Tag] 74 | } 75 | 76 | action createTask { 77 | fn: import { createTask } from "@src/tasks/actions", 78 | entities: [Task] 79 | } 80 | 81 | action updateTaskStatus { 82 | fn: import { updateTaskStatus } from "@src/tasks/actions", 83 | entities: [Task] 84 | } 85 | 86 | action deleteCompletedTasks { 87 | fn: import { deleteCompletedTasks } from "@src/tasks/actions", 88 | entities: [Task], 89 | } 90 | 91 | query getTags { 92 | fn: import { getTags } from "@src/tags/queries", 93 | entities: [Tag] 94 | } 95 | 96 | action createTag { 97 | fn: import { createTag } from "@src/tags/actions", 98 | entities: [Tag] 99 | } 100 | // #endregion Tasks 101 | -------------------------------------------------------------------------------- /basic/src/tags/components/CreateTagForm.tsx: -------------------------------------------------------------------------------- 1 | import { Controller, SubmitHandler, useForm } from "react-hook-form"; 2 | import { createTag } from "wasp/client/operations"; 3 | import { Input } from "../../shared/components/Input"; 4 | import { ColorRadioButtons } from "./ColorRadioButtons"; 5 | import { generateBrightColor } from "./colors"; 6 | import { TagLabel } from "./TagLabel"; 7 | 8 | interface CreateTagFormProps { 9 | onTagCreated: () => void; 10 | } 11 | 12 | interface CreateTagFormValues { 13 | name: string; 14 | color: string; 15 | } 16 | 17 | export const CREATE_TAG_FORM_ID = "create-tag"; 18 | 19 | export function CreateTagForm({ onTagCreated }: CreateTagFormProps) { 20 | const { handleSubmit, setValue, watch, control, reset } = 21 | useForm({ 22 | defaultValues: { 23 | name: "", 24 | color: generateBrightColor(), 25 | }, 26 | }); 27 | 28 | const onSubmit: SubmitHandler = async (data) => { 29 | try { 30 | await createTag(data); 31 | onTagCreated(); 32 | } catch (err: unknown) { 33 | window.alert(`Error while creating tag: ${String(err)}`); 34 | } finally { 35 | reset(); 36 | } 37 | }; 38 | 39 | const [name, color] = watch(["name", "color"]); 40 | 41 | return ( 42 |
    47 | ( 54 | 60 | )} 61 | /> 62 | setValue("color", color)} 65 | /> 66 | {name && ( 67 |
    68 | Preview 69 |
    70 | 75 | 80 |
    81 |
    82 | )} 83 | 84 | ); 85 | } 86 | 87 | /** 88 | * Calling `stopPropagation()` on `SubmitHandler`'s event does not stop the propagation properly. 89 | * So we use this wrapper instead. 90 | * 91 | * @see https://github.com/react-hook-form/documentation/issues/916 92 | */ 93 | function stopPropagate( 94 | callback: (event: React.FormEvent) => void, 95 | ) { 96 | return (e: React.FormEvent) => { 97 | e.stopPropagation(); 98 | callback(e); 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /basic/src/tasks/components/CreateTaskForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Controller, SubmitHandler, useForm } from "react-hook-form"; 3 | import { createTask, getTags, useQuery } from "wasp/client/operations"; 4 | import { Tag } from "wasp/entities"; 5 | import { Button } from "../../shared/components/Button"; 6 | import { Input } from "../../shared/components/Input"; 7 | import { CreateTagDialog } from "../../tags/components/CreateTagDialog"; 8 | import { TagLabel } from "../../tags/components/TagLabel"; 9 | 10 | interface CreateTaskFormValues { 11 | description: string; 12 | tagIds: string[]; 13 | } 14 | 15 | export function CreateTaskForm() { 16 | const { data: tags } = useQuery(getTags); 17 | const { handleSubmit, getValues, setValue, watch, control, reset } = 18 | useForm({ 19 | defaultValues: { 20 | description: "", 21 | tagIds: [], 22 | }, 23 | }); 24 | 25 | const onSubmit: SubmitHandler = async (data, event) => { 26 | event?.stopPropagation(); 27 | 28 | try { 29 | await createTask(data); 30 | } catch (err: unknown) { 31 | window.alert(`Error while creating task: ${String(err)}`); 32 | } finally { 33 | reset(); 34 | } 35 | }; 36 | 37 | const toggleTag = React.useCallback( 38 | function toggleTag(id: Tag["id"]) { 39 | const tagIds = getValues("tagIds"); 40 | if (tagIds.includes(id)) { 41 | setValue( 42 | "tagIds", 43 | tagIds.filter((tagId) => tagId !== id), 44 | ); 45 | } else { 46 | setValue("tagIds", [...tagIds, id]); 47 | } 48 | }, 49 | [getValues, setValue], 50 | ); 51 | 52 | const tagIds = watch("tagIds"); 53 | 54 | return ( 55 |
    60 |

    Create a new task

    61 | ( 68 | 74 | )} 75 | /> 76 | 77 |
    78 | Select tags 79 |
    80 | {tags && tags.length > 0 && ( 81 |
      82 | {tags.map((tag) => ( 83 |
    • 84 | 91 |
    • 92 | ))} 93 |
    94 | )} 95 | 96 |
    97 |
    98 | 101 | 102 | ); 103 | } 104 | --------------------------------------------------------------------------------