├── 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 |
13 |
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 |
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 |
34 |
41 |
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 |
17 | {label}
18 |
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 |
14 |
Todo App
15 |
16 |
17 |
18 | {user ? (
19 |
20 | Log out
21 |
22 | ) : (
23 | <>
24 |
25 | Sign up
26 |
27 |
28 |
29 | Login
30 |
31 |
32 | >
33 | )}
34 |
35 |
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 |
32 |
33 |
39 |
40 |
{task.description}
41 |
42 | {task.createdAt.toLocaleDateString()}
43 |
44 |
45 |
46 |
47 | {task.tags.map((tag) => (
48 |
49 |
50 |
51 | ))}
52 |
53 |
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 | setTagDialogOpen(true)}
17 | >
18 | Add a Tag
19 | +
20 |
21 | {tagDialogOpen && (
22 |
23 | setTagDialogOpen(false)}>
24 |
25 |
26 | Create a new tag
27 |
28 |
29 | setTagDialogOpen(false)} />
30 |
31 |
32 |
33 | Create
34 |
35 | setTagDialogOpen(false)}
39 | variant="ghost"
40 | >
41 | Cancel
42 |
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 |
51 | Clear completed
52 |
57 |
58 |
59 |
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 |
30 | {children}
31 |
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 |
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 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------