Merge branch 'release'
3
examples/todo-typescript/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/.wasp/
|
||||||
|
/.env.server
|
||||||
|
/.env.client
|
1
examples/todo-typescript/.wasproot
Normal file
@ -0,0 +1 @@
|
|||||||
|
File marking the root of Wasp project.
|
68
examples/todo-typescript/main.wasp
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
app TodoTypescript {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.7.3"
|
||||||
|
},
|
||||||
|
title: "ToDo TypeScript",
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
userEntity: User,
|
||||||
|
methods: {
|
||||||
|
usernameAndPassword: {},
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/login",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Prisma Schema Language (PSL) to define our entities: https://www.prisma.io/docs/concepts/components/prisma-schema
|
||||||
|
// Run `wasp db migrate-dev` in the CLI to create the database tables
|
||||||
|
// Then run `wasp db studio` to open Prisma Studio and view your db models
|
||||||
|
entity User {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
username String @unique
|
||||||
|
password String
|
||||||
|
tasks Task[]
|
||||||
|
psl=}
|
||||||
|
|
||||||
|
entity Task {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
description String
|
||||||
|
isDone Boolean @default(false)
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
userId Int?
|
||||||
|
psl=}
|
||||||
|
|
||||||
|
route RootRoute { path: "/", to: MainPage }
|
||||||
|
page MainPage {
|
||||||
|
authRequired: true,
|
||||||
|
component: import { MainPage } from "@client/MainPage"
|
||||||
|
}
|
||||||
|
|
||||||
|
route LoginRoute { path: "/login", to: LoginPage }
|
||||||
|
page LoginPage {
|
||||||
|
component: import { LoginPage } from "@client/LoginPage"
|
||||||
|
}
|
||||||
|
|
||||||
|
route SignupRoute { path: "/signup", to: SignupPage }
|
||||||
|
page SignupPage {
|
||||||
|
component: import { SignupPage } from "@client/SignupPage"
|
||||||
|
}
|
||||||
|
|
||||||
|
query getTasks {
|
||||||
|
// We specify the JS implementation of our query (which is an async JS function)
|
||||||
|
// Even if you use TS and have a queries.ts file, you will still need to import it using the .js extension.
|
||||||
|
// see here for more info: https://wasp-lang.dev/docs/tutorials/todo-app/03-listing-tasks#wasp-declaration
|
||||||
|
fn: import { getTasks } from "@server/queries.js",
|
||||||
|
// We tell Wasp that this query is doing something with the `Task` entity. With that, Wasp will
|
||||||
|
// automatically refresh the results of this query when tasks change.
|
||||||
|
entities: [Task]
|
||||||
|
}
|
||||||
|
|
||||||
|
action createTask {
|
||||||
|
fn: import { createTask } from "@server/actions.js",
|
||||||
|
entities: [Task]
|
||||||
|
}
|
||||||
|
|
||||||
|
action updateTask {
|
||||||
|
fn: import { updateTask } from "@server/actions.js",
|
||||||
|
entities: [Task]
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Task" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"isDone" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"userId" INTEGER,
|
||||||
|
CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
3
examples/todo-typescript/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
3
examples/todo-typescript/src/.waspignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Ignore editor tmp files
|
||||||
|
**/*~
|
||||||
|
**/#*#
|
16
examples/todo-typescript/src/client/LoginPage.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import LoginForm from '@wasp/auth/forms/Login';
|
||||||
|
|
||||||
|
export function LoginPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Login</h1>
|
||||||
|
{/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */}
|
||||||
|
<LoginForm />
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
I don't have an account yet (<Link to='/signup'>go to signup</Link>).
|
||||||
|
</span>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
53
examples/todo-typescript/src/client/Main.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
* {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 1rem 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.2rem;
|
||||||
|
background: #efefef;
|
||||||
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form h2 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasklist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
width: 300px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
88
examples/todo-typescript/src/client/MainPage.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import './Main.css';
|
||||||
|
import React, { useEffect, FormEventHandler, FormEvent } from 'react';
|
||||||
|
import logout from '@wasp/auth/logout.js';
|
||||||
|
import useAuth from '@wasp/auth/useAuth.js';
|
||||||
|
import { useQuery } from '@wasp/queries'; // Wasp uses a thin wrapper around react-query
|
||||||
|
import getTasks from '@wasp/queries/getTasks';
|
||||||
|
import createTask from '@wasp/actions/createTask';
|
||||||
|
import updateTask from '@wasp/actions/updateTask';
|
||||||
|
import waspLogo from './waspLogo.png';
|
||||||
|
import { Task } from './types'
|
||||||
|
|
||||||
|
export function MainPage() {
|
||||||
|
const { data: user } = useAuth();
|
||||||
|
const { data: tasks, isLoading, error } = useQuery<unknown, Task[]>(getTasks);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(user);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
if (isLoading) return 'Loading...';
|
||||||
|
if (error) return 'Error: ' + error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<img src={waspLogo} alt='wasp logo' />
|
||||||
|
<h1>
|
||||||
|
{user.username}
|
||||||
|
{`'s tasks :)`}
|
||||||
|
</h1>
|
||||||
|
<NewTaskForm />
|
||||||
|
{tasks && <TasksList tasks={tasks} /> }
|
||||||
|
<button onClick={logout}> Logout </button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Todo({ id, isDone, description }: Task) {
|
||||||
|
const handleIsDoneChange = async (event: FormEventHandler<HTMLInputElement>) => {
|
||||||
|
try {
|
||||||
|
await updateTask({
|
||||||
|
taskId: id,
|
||||||
|
isDone: event.currentTarget.checked,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
window.alert('Error while updating task ' + err?.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<input type='checkbox' id={id.toString()} checked={isDone} onChange={handleIsDoneChange} />
|
||||||
|
<span>{description}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function TasksList({tasks}: { tasks: Task[] }) {
|
||||||
|
if (tasks.length === 0) return <p>No tasks yet.</p>;
|
||||||
|
return (
|
||||||
|
<ol className='tasklist'>
|
||||||
|
{tasks.map((tsk, idx) => (
|
||||||
|
<Todo {...tsk} key={idx} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function NewTaskForm() {
|
||||||
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const description = event.currentTarget.description.value;
|
||||||
|
console.log(description)
|
||||||
|
event.currentTarget.reset();
|
||||||
|
await createTask({ description });
|
||||||
|
} catch (err: any) {
|
||||||
|
window.alert('Error: ' + err?.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input name='description' type='text' defaultValue='' />
|
||||||
|
<input type='submit' value='Create task' />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
16
examples/todo-typescript/src/client/SignupPage.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import SignupForm from '@wasp/auth/forms/Signup';
|
||||||
|
|
||||||
|
export function SignupPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Sign Up</h1>
|
||||||
|
{/** Wasp has built-in auth forms & flows, which you can also opt-out of, if you wish :) */}
|
||||||
|
<SignupForm />
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
I already have an account (<Link to='/login'>go to login</Link>).
|
||||||
|
</span>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
60
examples/todo-typescript/src/client/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
declare module '*.avif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.bmp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export const ReactComponent: React.FunctionComponent<React.SVGProps<
|
||||||
|
SVGSVGElement
|
||||||
|
> & { title?: string }>;
|
||||||
|
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
55
examples/todo-typescript/src/client/tsconfig.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// =============================== IMPORTANT =================================
|
||||||
|
//
|
||||||
|
// This file is only used for Wasp IDE support. You can change it to configure
|
||||||
|
// your IDE checks, but none of these options will affect the TypeScript
|
||||||
|
// compiler. Proper TS compiler configuration in Wasp is coming soon :)
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// JSX support
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
// Allow default imports.
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
// Wasp needs the following settings enable IDE support in your source
|
||||||
|
// files. Editing them might break features like import autocompletion and
|
||||||
|
// definition lookup. Don't change them unless you know what you're doing.
|
||||||
|
//
|
||||||
|
// The relative path to the generated web app's root directory. This must be
|
||||||
|
// set to define the "paths" option.
|
||||||
|
"baseUrl": "../../.wasp/out/web-app/",
|
||||||
|
"paths": {
|
||||||
|
// Resolve all "@wasp" imports to the generated source code.
|
||||||
|
"@wasp/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
// Resolve all non-relative imports to the correct node module. Source:
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
|
||||||
|
"*": [
|
||||||
|
// Start by looking for the definiton inside the node modules root
|
||||||
|
// directory...
|
||||||
|
"node_modules/*",
|
||||||
|
// ... If that fails, try to find it inside definitely-typed type
|
||||||
|
// definitions.
|
||||||
|
"node_modules/@types/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
|
||||||
|
"typeRoots": [
|
||||||
|
"../../.wasp/out/web-app/node_modules/@types"
|
||||||
|
],
|
||||||
|
// Since this TS config is used only for IDE support and not for
|
||||||
|
// compilation, the following directory doesn't exist. We need to specify
|
||||||
|
// it to prevent this error:
|
||||||
|
// https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file
|
||||||
|
"outDir": "phantom"
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"phantom"
|
||||||
|
],
|
||||||
|
}
|
6
examples/todo-typescript/src/client/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type Task = {
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
isDone: boolean;
|
||||||
|
userId: number | null;
|
||||||
|
};
|
BIN
examples/todo-typescript/src/client/waspLogo.png
Normal file
After Width: | Height: | Size: 24 KiB |
34
examples/todo-typescript/src/server/actions.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import HttpError from '@wasp/core/HttpError.js';
|
||||||
|
import { Context, Task } from './serverTypes'
|
||||||
|
|
||||||
|
type CreateArgs = Pick<Task, 'description'>;
|
||||||
|
|
||||||
|
export async function createTask({ description }: CreateArgs, context: Context) {
|
||||||
|
if (!context.user) {
|
||||||
|
throw new HttpError(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.entities.Task.create({
|
||||||
|
data: {
|
||||||
|
description,
|
||||||
|
user: { connect: { id: context.user.id } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// type UpdateArgs = { taskId: Task['id']; isDone: Task['isDone'] };
|
||||||
|
type UpdateArgs = Pick<Task, 'id' | 'isDone'>;
|
||||||
|
|
||||||
|
export async function updateTask({ id, isDone }: UpdateArgs, context: Context) {
|
||||||
|
if (!context.user) {
|
||||||
|
throw new HttpError(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.entities.Task.updateMany({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
user: { id: context.user.id },
|
||||||
|
},
|
||||||
|
data: { isDone },
|
||||||
|
});
|
||||||
|
};
|
9
examples/todo-typescript/src/server/queries.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import HttpError from '@wasp/core/HttpError.js';
|
||||||
|
import { Context, Task } from './serverTypes'
|
||||||
|
|
||||||
|
export async function getTasks(args: unknown, context: Context): Promise<Task[]> {
|
||||||
|
if (!context.user) {
|
||||||
|
throw new HttpError(401);
|
||||||
|
}
|
||||||
|
return context.entities.Task.findMany({ where: { user: { id: context.user.id } } });
|
||||||
|
};
|
11
examples/todo-typescript/src/server/serverTypes.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { User, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
export { Task } from '@prisma/client';
|
||||||
|
|
||||||
|
export type Context = {
|
||||||
|
user: User;
|
||||||
|
entities: {
|
||||||
|
Task: Prisma.TaskDelegate<{}>;
|
||||||
|
User: Prisma.UserDelegate<{}>;
|
||||||
|
};
|
||||||
|
};
|
48
examples/todo-typescript/src/server/tsconfig.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// =============================== IMPORTANT =================================
|
||||||
|
//
|
||||||
|
// This file is only used for Wasp IDE support. You can change it to configure
|
||||||
|
// your IDE checks, but none of these options will affect the TypeScript
|
||||||
|
// compiler. Proper TS compiler configuration in Wasp is coming soon :)
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Allows default imports.
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
// Wasp needs the following settings enable IDE support in your source
|
||||||
|
// files. Editing them might break features like import autocompletion and
|
||||||
|
// definition lookup. Don't change them unless you know what you're doing.
|
||||||
|
//
|
||||||
|
// The relative path to the generated web app's root directory. This must be
|
||||||
|
// set to define the "paths" option.
|
||||||
|
"baseUrl": "../../.wasp/out/server/",
|
||||||
|
"paths": {
|
||||||
|
// Resolve all "@wasp" imports to the generated source code.
|
||||||
|
"@wasp/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
// Resolve all non-relative imports to the correct node module. Source:
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
|
||||||
|
"*": [
|
||||||
|
// Start by looking for the definiton inside the node modules root
|
||||||
|
// directory...
|
||||||
|
"node_modules/*",
|
||||||
|
// ... If that fails, try to find it inside definitely-typed type
|
||||||
|
// definitions.
|
||||||
|
"node_modules/@types/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
|
||||||
|
"typeRoots": [
|
||||||
|
"../../.wasp/out/server/node_modules/@types"
|
||||||
|
],
|
||||||
|
// Since this TS config is used only for IDE support and not for
|
||||||
|
// compilation, the following directory doesn't exist. We need to specify
|
||||||
|
// it to prevent this error:
|
||||||
|
// https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file
|
||||||
|
"outDir": "phantom",
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"phantom"
|
||||||
|
],
|
||||||
|
}
|
28
examples/todo-typescript/src/shared/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable default imports in TypeScript.
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
|
// The following settings enable IDE support in user-provided source files.
|
||||||
|
// Editing them might break features like import autocompletion and
|
||||||
|
// definition lookup. Don't change them unless you know what you're doing.
|
||||||
|
//
|
||||||
|
// The relative path to the generated web app's root directory. This must be
|
||||||
|
// set to define the "paths" option.
|
||||||
|
"baseUrl": "../../.wasp/out/server/",
|
||||||
|
"paths": {
|
||||||
|
// Resolve all non-relative imports to the correct node module. Source:
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
|
||||||
|
"*": [
|
||||||
|
// Start by looking for the definiton inside the node modules root
|
||||||
|
// directory...
|
||||||
|
"node_modules/*",
|
||||||
|
// ... If that fails, try to find it inside definitely-typed type
|
||||||
|
// definitions.
|
||||||
|
"node_modules/@types/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Correctly resolve types: https://www.typescriptlang.org/tsconfig#typeRoots
|
||||||
|
"typeRoots": ["../../.wasp/out/server/node_modules/@types"],
|
||||||
|
}
|
||||||
|
}
|
@ -16,9 +16,9 @@ import ImgWithCaption from './components/ImgWithCaption'
|
|||||||
![amicus hero shot](../static/img/amicus-usecase/amicus-hero-shot.png)
|
![amicus hero shot](../static/img/amicus-usecase/amicus-hero-shot.png)
|
||||||
|
|
||||||
|
|
||||||
[Erlis Kllogjri](https://github.com/ErlisK) is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how Amicus started out.
|
[Erlis Kllogjri](https://github.com/ErlisK) is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how [Amicus](https://www.amicus.work/) started out.
|
||||||
|
|
||||||
Amicus is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.
|
[Amicus](https://www.amicus.work/) is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.
|
||||||
|
|
||||||
Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!
|
Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!
|
||||||
|
|
||||||
|
132
web/blog/2023-01-11-betathon-review.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
title: 'Hosting Our First Hackathon: Results & Review'
|
||||||
|
authors: [vinny]
|
||||||
|
tags: [fullstack, webdev, hackathon, startups]
|
||||||
|
---
|
||||||
|
|
||||||
|
import ImgWithCaption from './components/ImgWithCaption'
|
||||||
|
|
||||||
|
To finalize the Wasp Beta launch week, we held a Beta Hackathon, which we dubbed the “Betathon”. The idea was to hold a simple, open, and fun hackathon to encourage users to build with Wasp, and that’s exactly what they did!
|
||||||
|
|
||||||
|
As Wasp is still in its early days, we weren’t sure what the response would be, or if there’d be any response at all. Considering that we didn’t do much promotion of the Hackathon outside of our own channels, we were surprised by the results.
|
||||||
|
|
||||||
|
In this post, I’ll give you a quick run-down of:
|
||||||
|
|
||||||
|
- the hackathon results 🏆
|
||||||
|
- how the hackathon was organized
|
||||||
|
- how we promoted it
|
||||||
|
- the community response
|
||||||
|
|
||||||
|
|
||||||
|
## …and the Winners Are:
|
||||||
|
|
||||||
|
What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍
|
||||||
|
|
||||||
|
|
||||||
|
### 🥇 Tim’s Job Board
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Tim's Job Board"
|
||||||
|
source="img/betathon/tim.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Tim really went for it and created a feature-rich Job Board:
|
||||||
|
|
||||||
|
- View the [App](https://client-production-54e7.up.railway.app/) & [GitHub Repo](https://github.com/tskaggs/wasp-jobs)
|
||||||
|
- Follow [Tim on Twitter](https://twitter.com/tskaggs)
|
||||||
|
- 🎉 Prizes: Wasp-colored Mechanical Keyboard, Wasp swag, $200 [Railway.app](http://Railway.app) credits
|
||||||
|
|
||||||
|
|
||||||
|
> “***Wasp is very awesome!*** *Easy setup and start-up especially if you're familiar with the Prisma ORM and Tailwind CSS. The stack is small but powerful... I'm going to use Wasp on a few MVP projects this year.”* - Tim
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
### 🥈Chris’s “Cook Wherever” Recipes App
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Chris's Cook Wherever Recipes App"
|
||||||
|
source="img/betathon/chris.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Chris created an extensive database of recipes in a slick app:
|
||||||
|
|
||||||
|
- View the [App](https://cookwherever.com) & [GitHub Repo](https://github.com/cookwherever/cookwherever)
|
||||||
|
- Follow [Chris on Twitter](https://twitter.com/breadchris)
|
||||||
|
- 🎉 Prizes: Wasp swag, $125 [Railway.app](http://Railway.app) credits
|
||||||
|
|
||||||
|
> “***This was the best app dev experience I ever had!*** *…Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.”* - Chris
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
### 🥉 Richard’s Roadmap & Feature Voting App
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Richard’s Roadmap & Feature Voting App"
|
||||||
|
source="img/betathon/richard.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
- View the [App](https://droad.netlify.app/) & [GitHub Repo](https://github.com/Fecony/droad)
|
||||||
|
- Follow [Richard on Twitter](https://twitter.com/webrickony)
|
||||||
|
- 🎉 Prizes: Wasp Shirt, $75 [Railway.app](http://Railway.app) credits
|
||||||
|
|
||||||
|
> “***I liked how Wasp simplified writing query/actions*** *that are used to interact with the backend and frontend. How everything is defined and configured in wasp file and just works. Also […] login/signup was really easy to do since Wasp provides these two methods for use.”* -
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
### 🥉 Emmanuel’s Notes App
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Emmanuel’s Notes App"
|
||||||
|
source="img/betathon/emmanuel.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
- View the [GitHub Repo](https://github.com/EmmanuelTheCoder/noteapp-with-wasp)
|
||||||
|
- Follow [Emmanuel on Twitter](https://twitter.com/EmmanuelCoder)
|
||||||
|
- 🎉 Prizes: Wasp Shirt, $75 [Railway.app](http://Railway.app) credits
|
||||||
|
|
||||||
|
> *I joined the hackathon less than 48 hours before the submission deadline.* ***Wasp made it look easy because it handled the hard parts for me.*** *For example, username/password authentication took less than 7 lines of code to implement. -* excerpt from [Emmanuel’s Betathon Blog Post](https://dev.to/emmanuelthecoder/making-something-waspy-a-review-of-wasp-571j)
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
## Hackathon How-to
|
||||||
|
|
||||||
|
Personally, I’ve never organized a hackathon before, and this was Wasp’s first hackathon as well, so when you’re a complete newbie at something, you often look towards others for inspiration. Being admirers of the work and style of Supabase, we drew a lot of inspiration from their “[launch week](https://supabase.com/blog/launch-week-5-hackathon)” approach when preparing for our own Beta launch and hacakthon.
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Wasp Betathon Homepage"
|
||||||
|
source="img/betathon/betathonpage.png"
|
||||||
|
caption="Our dedicated hackathon landing page w/ intro video & submission form"
|
||||||
|
/>
|
||||||
|
|
||||||
|
With some good inspiration in hand, we set off to create a simple, easy-going Hackathon experience. We weren’t certain we’d get many participants, so we decided to make the process as open as possible: *two weeks to work on any project using Wasp, alone or in a team of up to 4 people, submitted on our [Betathon Homepage](https://betathon.wasp-lang.dev/) before the deadline*. That was it.
|
||||||
|
|
||||||
|
When you’re an early-stage startup, you can’t offer big cash prizes, so we asked Railway if they’d be interested in sponsoring some prizes, as we’re big fans of their deployment and hosting platform. Luckily, they agreed (thanks, Railway 🙏🚂). It was also a great match, since we already had the documentation for deploying Wasp apps to Railway on our website, making it an obvious choice for the participants to deploy their Hackathon apps with.
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Keyboard"
|
||||||
|
source="img/betathon/keyboard.png"
|
||||||
|
caption="Disclaimer: actual prize keyboard will be cooler and waspier 😎🐝"
|
||||||
|
/>
|
||||||
|
|
||||||
|
On top of that, we decided that a cool grand prize could be a Wasp-colored mechanical keyboard. Nothing fancy, but keyboards are an item a lot of programmers love. We also threw in some Wasp beanies and shirts, and stated that we’d spotlight the winner’s on our platforms and social media accounts.
|
||||||
|
|
||||||
|
|
||||||
|
## Promotion
|
||||||
|
|
||||||
|
For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didn’t outright promote the hackathon on those platforms, but we were getting a lot of incoming interest to our Website and Discord, so we made noise about it there. We posted banners on the homepage, and made announcements on Discord and Twitter that directed people to a [Beta Hacakthon homepage](https://betathon.wasp-lang.dev) we created.
|
||||||
|
|
||||||
|
The homepage was nice to have as a central spot for all the rules and relevant info. We also added a fun intro video to give the hackathon a more personal touch. I also think the effort put into making an intro video gives participants the feeling that they’re entering into a serious contest and committing to something of substance.
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Hackathon Wasp app repo"
|
||||||
|
source="img/betathon/github.png"
|
||||||
|
caption="Wanna host your own Hackathon? Use our template app!"
|
||||||
|
/>
|
||||||
|
|
||||||
|
As an extra bonus, we wrote the Betathon Homepage with Wasp, and put the [source code up on our GitHub](https://github.com/wasp-lang/wasp/tree/main/examples/hackathon). We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻
|
||||||
|
|
||||||
|
### The Response
|
||||||
|
|
||||||
|
The response overall was small but significant, considering Wasp’s age. We were also extremely happy with the quality of the engagement. We had thirteen participants register overall, a nice number considering we only started promoting the hackathon on the day that we announced it (this is probably something we’d do differently next time)!
|
||||||
|
|
||||||
|
We also asked participants for their feedback on participating in the Hackathon, and they were all pleased with the open, straight-forward approach we took, so we’ll most likely be repeating this for future versions. Other good signs were the many comments that participants were eager to take part in our next hackathon, as well as some dedicated new community members, which makes it all the more motivating for us. 💪
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**A big THANK YOU again to all the participants for their hard work and feedback. Here’s to the next one! 🍻**
|
@ -4,10 +4,12 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
|
|||||||
const ImgWithCaption = (props) => {
|
const ImgWithCaption = (props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p align="center">
|
<p align='center'>
|
||||||
<figure>
|
<figure>
|
||||||
<img style={{'width': props.width}} alt={props.alt} src={useBaseUrl(props.source)} />
|
<img style={{ width: props.width }} alt={props.alt} src={useBaseUrl(props.source)} />
|
||||||
<figcaption class="image-caption">{props.caption}</figcaption>
|
<figcaption class='image-caption' style={{ fontStyle: 'italic', opacity: 0.6, fontSize: '0.9rem' }}>
|
||||||
|
{props.caption}
|
||||||
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
5
web/docs/_addExternalAuthEnvVarsReminder.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
:::tip Using an external auth method?
|
||||||
|
|
||||||
|
if your app is using an external authentication method(s) supported by Wasp (such as [Google](/docs/language/features#google) or [GitHub](/docs/language/features#github)), make sure to set the necessary environment variables.
|
||||||
|
|
||||||
|
:::
|
@ -3,6 +3,8 @@ title: Deploying
|
|||||||
---
|
---
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
import AddExternalAuthEnvVarsReminder from './_addExternalAuthEnvVarsReminder.md'
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly a bit bigger changes in the future.
|
Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly a bit bigger changes in the future.
|
||||||
If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
|
If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
|
||||||
@ -43,6 +45,8 @@ Server uses following environment variables, so you need to ensure they are set
|
|||||||
- `WASP_WEB_CLIENT_URL` -> The URL of where the frontend app is running (e.g. `https://<app-name>.netlify.app`), which is necessary for CORS.
|
- `WASP_WEB_CLIENT_URL` -> The URL of where the frontend app is running (e.g. `https://<app-name>.netlify.app`), which is necessary for CORS.
|
||||||
- `JWT_SECRET` -> You need this if you are using Wasp's `auth` feature. Set it to a random string (password), at least 32 characters long.
|
- `JWT_SECRET` -> You need this if you are using Wasp's `auth` feature. Set it to a random string (password), at least 32 characters long.
|
||||||
|
|
||||||
|
<AddExternalAuthEnvVarsReminder />
|
||||||
|
|
||||||
### Deploying to Fly.io (free, recommended)
|
### Deploying to Fly.io (free, recommended)
|
||||||
|
|
||||||
Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/).
|
Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/).
|
||||||
@ -99,8 +103,14 @@ Next, let's add a few more environment variables:
|
|||||||
flyctl secrets set PORT=8080
|
flyctl secrets set PORT=8080
|
||||||
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
|
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
|
||||||
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
|
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
|
||||||
|
|
||||||
|
# If you are using an external auth method (Google or GitHub), make sure to add their vars too!
|
||||||
|
# flyctl secrets set GOOGLE_CLIENT_ID=<google_client_id>
|
||||||
|
# flyctl secrets set GOOGLE_CLIENT_SECRET=<google_client_secret>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<AddExternalAuthEnvVarsReminder />
|
||||||
|
|
||||||
NOTE: If you do not know what your frontend URL is yet, don't worry. You can set `WASP_WEB_CLIENT_URL` after you deploy your frontend.
|
NOTE: If you do not know what your frontend URL is yet, don't worry. You can set `WASP_WEB_CLIENT_URL` after you deploy your frontend.
|
||||||
|
|
||||||
If you want to make sure you've added your secrets correctly, run `flyctl secrets list` in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.
|
If you want to make sure you've added your secrets correctly, run `flyctl secrets list` in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.
|
||||||
@ -156,8 +166,13 @@ heroku create <app-name>
|
|||||||
Unless you have external Postgres database that you want to use, let's create new database on Heroku and attach it to our app:
|
Unless you have external Postgres database that you want to use, let's create new database on Heroku and attach it to our app:
|
||||||
|
|
||||||
```
|
```
|
||||||
heroku addons:create --app <app-name> heroku-postgresql:hobby-dev
|
heroku addons:create --app <app-name> heroku-postgresql:mini
|
||||||
```
|
```
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
Heroku does not offer a free plan anymore and `mini` is their cheapest database instance - it costs $5/mo.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
Heroku will also set `DATABASE_URL` env var for us at this point. If you are using external database, you will have to set it yourself.
|
Heroku will also set `DATABASE_URL` env var for us at this point. If you are using external database, you will have to set it yourself.
|
||||||
|
|
||||||
@ -375,6 +390,8 @@ Go to the server instance's `Settings` tab, and click `Generate Domain`. Do the
|
|||||||
The Postgres database is already initialized with a domain, so click on the Postgres instance, go to the **Connect** tab and copy the `Postgres Connection URL`.
|
The Postgres database is already initialized with a domain, so click on the Postgres instance, go to the **Connect** tab and copy the `Postgres Connection URL`.
|
||||||
|
|
||||||
Go back to your `server` instance and navigate to its `Variables` tab. Now add the copied Postgres URL as `DATABASE_URL`, as well as the client's domain as `WASP_WEB_CLIENT_URL`.
|
Go back to your `server` instance and navigate to its `Variables` tab. Now add the copied Postgres URL as `DATABASE_URL`, as well as the client's domain as `WASP_WEB_CLIENT_URL`.
|
||||||
|
|
||||||
|
<AddExternalAuthEnvVarsReminder />
|
||||||
|
|
||||||
Next, copy the server's domain, move over to the client's `Variables` tab and add the generated server domain as a new variable called `REACT_APP_API_URL`.
|
Next, copy the server's domain, move over to the client's `Variables` tab and add the generated server domain as a new variable called `REACT_APP_API_URL`.
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ module.exports = {
|
|||||||
'@docusaurus/preset-classic',
|
'@docusaurus/preset-classic',
|
||||||
{
|
{
|
||||||
gtag: {
|
gtag: {
|
||||||
trackingID: 'G-3ZEDH3BVGE',
|
trackingID: 'GTM-WJX89HZ',
|
||||||
anonymizeIP: true,
|
anonymizeIP: true,
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
|
@ -13,7 +13,7 @@ const examples = [
|
|||||||
authorImg: 'https://avatars.githubusercontent.com/u/55102317',
|
authorImg: 'https://avatars.githubusercontent.com/u/55102317',
|
||||||
repoName: "waspello-example-app",
|
repoName: "waspello-example-app",
|
||||||
repoUrl: "https://github.com/wasp-lang/wasp/tree/main/examples/waspello",
|
repoUrl: "https://github.com/wasp-lang/wasp/tree/main/examples/waspello",
|
||||||
//demoUrl: "https://waspello.netlify.app/",
|
demoUrl: "https://waspello-demo.netlify.app/",
|
||||||
// todo: try in GitPod/Replit url
|
// todo: try in GitPod/Replit url
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -122,9 +122,12 @@ const ExampleCard = (props) => (
|
|||||||
<div className='mt-3 flex items-center gap-2'>
|
<div className='mt-3 flex items-center gap-2'>
|
||||||
|
|
||||||
<SeeTheCodeButton repoUrl={props.repoUrl} />
|
<SeeTheCodeButton repoUrl={props.repoUrl} />
|
||||||
{props.demoUrl && (
|
{/* Demo apps are not mobile-friendly yet so hiding them on mobile for now. */}
|
||||||
<DemoButton demoUrl={props.demoUrl} />
|
<span className='hidden md:block'>
|
||||||
)}
|
{props.demoUrl && (
|
||||||
|
<DemoButton demoUrl={props.demoUrl} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
BIN
web/static/img/betathon/betathonpage.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
web/static/img/betathon/chris.png
Normal file
After Width: | Height: | Size: 780 KiB |
BIN
web/static/img/betathon/emmanuel.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
web/static/img/betathon/github.png
Normal file
After Width: | Height: | Size: 669 KiB |
BIN
web/static/img/betathon/keyboard.png
Normal file
After Width: | Height: | Size: 839 KiB |
BIN
web/static/img/betathon/richard.png
Normal file
After Width: | Height: | Size: 367 KiB |
BIN
web/static/img/betathon/tim.png
Normal file
After Width: | Height: | Size: 621 KiB |