mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-05-21 09:40:07 +02:00
Openai demo app vertical reorg (#234)
* organize demo ai app vertically * Update main.wasp.diff * Update guided-tour.md * Leftover vertical org changes (#233) * newsletter dir & leftovers * update app_diff * Update guided-tour.md * Update guided-tour.md
This commit is contained in:
parent
c9d43586bb
commit
f52bc42de1
@ -84,7 +84,7 @@
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -195,7 +193,10 @@
|
||||
@@ -125,7 +123,10 @@
|
||||
email String? @unique
|
||||
username String? @unique
|
||||
lastActiveTimestamp DateTime @default(now())
|
||||
|
@ -22,8 +22,8 @@
|
||||
+} from './contentSections';
|
||||
import DropdownUser from '../../user/DropdownUser';
|
||||
import { UserMenuItems } from '../../user/UserMenuItems';
|
||||
-import { DocsUrl } from '../../common';
|
||||
+import { DocsUrl, GithubUrl } from '../../common';
|
||||
-import { DocsUrl } from '../../shared/common';
|
||||
+import { DocsUrl, GithubUrl } from '../../shared/common';
|
||||
import DarkModeSwitcher from '../components/DarkModeSwitcher';
|
||||
|
||||
export default function LandingPage() {
|
||||
|
@ -1,11 +1,11 @@
|
||||
--- template/app/src/client/landing-page/contentSections.ts
|
||||
+++ opensaas-sh/app/src/client/landing-page/contentSections.ts
|
||||
@@ -1,74 +1,126 @@
|
||||
-import { DocsUrl, BlogUrl } from '../../common';
|
||||
-import { DocsUrl, BlogUrl } from '../../shared/common';
|
||||
-import daBoiAvatar from '../static/da-boi.png';
|
||||
-import avatarPlaceholder from '../static/avatar-placeholder.png';
|
||||
-import { routes } from 'wasp/client/router';
|
||||
+import { DocsUrl, BlogUrl, GithubUrl } from '../../common';
|
||||
+import { DocsUrl, BlogUrl, GithubUrl } from '../../shared/common';
|
||||
|
||||
export const navigation = [
|
||||
{ name: 'Features', href: '#features' },
|
||||
|
@ -1,6 +1,7 @@
|
||||
--- template/app/src/common.ts
|
||||
+++ opensaas-sh/app/src/common.ts
|
||||
--- template/app/src/shared/common.ts
|
||||
+++ opensaas-sh/app/src/shared/common.ts
|
||||
@@ -1,2 +1,3 @@
|
||||
export const DocsUrl = 'https://docs.opensaas.sh';
|
||||
export const BlogUrl = 'https://docs.opensaas.sh/blog';
|
||||
+export const GithubUrl = 'https://github.com/wasp-lang/open-saas';
|
||||
\ No newline at end of file
|
@ -47,10 +47,14 @@ At the root of our project, you will see three folders:
|
||||
|
||||
`e2e-tests` contains the end-to-end tests using Playwright, which you can run to test your app's functionality.
|
||||
|
||||
### App File Structure
|
||||
|
||||
We've structured this full-stack app template vertically (by feature). That means that most directories within `app/src` contain both the React client code and NodeJS server code necessary for implementing its logic.
|
||||
|
||||
Let's check out what's in the `app` folder in more detail:
|
||||
|
||||
:::caution[v0.11 and below]
|
||||
If you are using a version of the OpenSaaS template with Wasp `v0.11.x` or below, you may see a slightly different file structure. But don't worry, the vast majority of the code and features are the same! 😅
|
||||
:::caution[v0.13 and below]
|
||||
If you are using an older version of the OpenSaaS template with Wasp `v0.13.x` or below, you may see a slightly different file structure. But don't worry, the vast majority of the code and features are the same! 😅
|
||||
:::
|
||||
|
||||
```sh
|
||||
@ -59,11 +63,17 @@ If you are using a version of the OpenSaaS template with Wasp `v0.11.x` or below
|
||||
├── .wasp/ # Output dir for Wasp. DON'T MODIFY THESE FILES!
|
||||
├── public/ # Public assets dir, e.g. www.yourdomain.com/banner.png
|
||||
├── src/ # Your code goes here.
|
||||
│ ├── client/ # Your client code (React) goes here.
|
||||
│ ├── server/ # Your server code (NodeJS) goes here.
|
||||
│ ├── admin/ # Admin dashboard related pages and components.
|
||||
│ ├── analytics/ # Logic and background jobs for processing analytics.
|
||||
│ ├── auth/ # All auth-related pages/components and logic.
|
||||
│ ├── client/ # Shared components, hooks, landing page, and other client code (React).
|
||||
│ ├── demo-ai-app/ # Logic for the example AI-powered demo app.
|
||||
│ ├── file-upload/ # Logic for uploading files to S3.
|
||||
│ ├── messages # Logic for app user messages.
|
||||
│ ├── newsletter/ # Logic for scheduled recurring newsletter sending.
|
||||
│ ├── payment/ # Logic for handling Stripe payments and webhooks.
|
||||
│ ├── server/ # Scripts, shared server utils, and other server-specific code (NodeJS).
|
||||
│ ├── shared/ # Shared constants and util functions.
|
||||
│ └── user/ # Logic related to users and their accounts.
|
||||
├── .env.server # Dev environment variables for your server code.
|
||||
├── .env.client # Dev environment variables for your client code.
|
||||
@ -71,14 +81,9 @@ If you are using a version of the OpenSaaS template with Wasp `v0.11.x` or below
|
||||
├── tailwind.config.js # TailwindCSS configuration.
|
||||
├── package.json
|
||||
├── package-lock.json
|
||||
|
||||
└── .wasproot
|
||||
```
|
||||
|
||||
:::tip[File Structure]
|
||||
Note that since Wasp v0.12, the `src` folder does not need to be organized between `client` and `server` code. You can organize your code however you like, e.g. by feature, but we've chosen to keep the traditional structure for this template.
|
||||
:::
|
||||
|
||||
### The Wasp Config file
|
||||
|
||||
This template at its core is a Wasp project, where [Wasp](https://wasp-lang.dev) is a full-stack web app framework that let’s you write your app in React, NodeJS, and Prisma and will manage the "boilerplatey" work for you, allowing you to just take care of the fun stuff!
|
||||
@ -104,35 +109,31 @@ It's possible to learn Wasp's feature set simply through using this template, bu
|
||||
|
||||
### Client
|
||||
|
||||
The `src/client` folder contains the code that runs in the browser. It's a standard React app, with a few Wasp-specific things sprinkled in.
|
||||
The `src/client` folder contains any additional client-side code that doesn't belong to a feature:
|
||||
|
||||
```sh
|
||||
.
|
||||
└── client
|
||||
├── admin # Admin dashboard pages and components
|
||||
├── app # Your user-facing app that sits behind the paywall/login.
|
||||
├── components # Your shared React components.
|
||||
├── hooks # Your shared React hooks.
|
||||
├── landing-page # Landing page related code
|
||||
├── static # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'
|
||||
├── App.tsx # Main app component to wrap all child components. Useful for global state, navbars, etc.
|
||||
└── Main.css
|
||||
├── components # Your shared React components.
|
||||
├── fonts # Extra fonts
|
||||
├── hooks # Your shared React hooks.
|
||||
├── icons # Your shared SVG icons.
|
||||
├── landing-page # Landing page related code
|
||||
├── static # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'
|
||||
├── App.tsx # Main app component to wrap all child components. Useful for global state, navbars, etc.
|
||||
├── cn.ts # Helper function for dynamic and conditional Tailwind CSS classes.
|
||||
└── Main.css
|
||||
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
The `src/server` folder contains the code that runs on the server. Wasp compiles everything into a NodeJS server for you.
|
||||
|
||||
All you have to do is define your server-side functions in the `main.wasp` file, write the logic in a function within `src/server` and Wasp will generate the boilerplate code for you.
|
||||
The `src/server` folder contains any additional server-side code that does not belong to a specific feature:
|
||||
|
||||
```sh
|
||||
└── server
|
||||
├── scripts # Scripts to run via Wasp, e.g. database seeding.
|
||||
├── workers # Functions that run in the background as Wasp Jobs, e.g. daily stats calculation.
|
||||
├── actions.ts # Your server-side write/mutation functions.
|
||||
├── queries.ts # Your server-side read functions.
|
||||
└── utils.ts
|
||||
├── scripts # Scripts to run via Wasp, e.g. database seeding.
|
||||
└── utils.ts
|
||||
```
|
||||
|
||||
## Main Features
|
||||
@ -257,7 +258,7 @@ To do that, we've leveraged Wasp's [Jobs feature](https://wasp-lang.dev/docs/adv
|
||||
job dailyStatsJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { calculateDailyStats } from "@src/server/workers/calculateDailyStats.js"
|
||||
fn: import { calculateDailyStats } from "@src/analytics/stats"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 * * * *" // runs every hour
|
||||
|
@ -85,29 +85,6 @@ app OpenSaaS {
|
||||
},
|
||||
}
|
||||
|
||||
entity GptResponse {=psl
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
content String
|
||||
psl=}
|
||||
|
||||
entity Task {=psl
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
description String
|
||||
time String @default("1")
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
|
||||
route LandingPageRoute { path: "/", to: LandingPage }
|
||||
page LandingPage {
|
||||
component: import LandingPage from "@src/client/landing-page/LandingPage"
|
||||
@ -140,53 +117,6 @@ page EmailVerificationPage {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
route DemoAppRoute { path: "/demo-app", to: DemoAppPage }
|
||||
page DemoAppPage {
|
||||
authRequired: true,
|
||||
component: import DemoAppPage from "@src/client/app/DemoAppPage"
|
||||
}
|
||||
|
||||
action generateGptResponse {
|
||||
fn: import { generateGptResponse } from "@src/server/actions.js",
|
||||
entities: [User, Task, GptResponse]
|
||||
}
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@src/server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action deleteTask {
|
||||
fn: import { deleteTask } from "@src/server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@src/server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
query getGptResponses {
|
||||
fn: import { getGptResponses } from "@src/server/queries.js",
|
||||
entities: [User, GptResponse]
|
||||
}
|
||||
|
||||
query getAllTasksByUser {
|
||||
fn: import { getAllTasksByUser } from "@src/server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
job emailChecker {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { checkAndQueueEmails } from "@src/server/workers/checkAndQueueEmails.js"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 7 * * 1" // at 7:00 am every Monday
|
||||
},
|
||||
entities: [User]
|
||||
}
|
||||
|
||||
//#region User
|
||||
entity User {=psl
|
||||
id String @id @default(uuid())
|
||||
@ -201,7 +131,7 @@ entity User {=psl
|
||||
checkoutSessionId String?
|
||||
subscriptionStatus String? // 'active', 'canceled', 'past_due', 'deleted'
|
||||
subscriptionPlan String? // 'hobby', 'pro'
|
||||
sendEmail Boolean @default(false)
|
||||
sendNewsletter Boolean @default(false)
|
||||
datePaid DateTime?
|
||||
credits Int @default(3)
|
||||
|
||||
@ -233,6 +163,67 @@ action updateUserById {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Demo AI App
|
||||
route DemoAppRoute { path: "/demo-app", to: DemoAppPage }
|
||||
page DemoAppPage {
|
||||
authRequired: true,
|
||||
component: import DemoAppPage from "@src/demo-ai-app/DemoAppPage"
|
||||
}
|
||||
|
||||
action generateGptResponse {
|
||||
fn: import { generateGptResponse } from "@src/demo-ai-app/operations",
|
||||
entities: [User, Task, GptResponse]
|
||||
}
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@src/demo-ai-app/operations",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action deleteTask {
|
||||
fn: import { deleteTask } from "@src/demo-ai-app/operations",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@src/demo-ai-app/operations",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
query getGptResponses {
|
||||
fn: import { getGptResponses } from "@src/demo-ai-app/operations",
|
||||
entities: [User, GptResponse]
|
||||
}
|
||||
|
||||
query getAllTasksByUser {
|
||||
fn: import { getAllTasksByUser } from "@src/demo-ai-app/operations",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
entity GptResponse {=psl
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
content String
|
||||
psl=}
|
||||
|
||||
entity Task {=psl
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
description String
|
||||
time String @default("1")
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
//#endregion
|
||||
|
||||
//#region Payment
|
||||
route PricingPageRoute { path: "/pricing", to: PricingPage }
|
||||
page PricingPage {
|
||||
@ -426,3 +417,16 @@ entity ContactFormMessage {=psl
|
||||
repliedAt DateTime?
|
||||
psl=}
|
||||
//#endregion
|
||||
|
||||
//#region Newsletter
|
||||
job sendNewsletter {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { checkAndQueueNewsletterEmails } from "@src/newsletter/sendNewsletter"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 7 * * 1" // at 7:00 am every Monday
|
||||
},
|
||||
entities: [User]
|
||||
}
|
||||
//#endregion
|
@ -8,7 +8,7 @@ import { HiBars3 } from 'react-icons/hi2';
|
||||
import logo from '../static/logo.png';
|
||||
import DropdownUser from '../../user/DropdownUser';
|
||||
import { UserMenuItems } from '../../user/UserMenuItems';
|
||||
import { DocsUrl, BlogUrl } from '../../common';
|
||||
import { DocsUrl, BlogUrl } from '../../shared/common';
|
||||
import DarkModeSwitcher from './DarkModeSwitcher';
|
||||
|
||||
const navigation = [
|
||||
|
@ -10,7 +10,7 @@ import openSaasBanner from '../static/open-saas-banner.png';
|
||||
import { features, navigation, faqs, footerNavigation, testimonials } from './contentSections';
|
||||
import DropdownUser from '../../user/DropdownUser';
|
||||
import { UserMenuItems } from '../../user/UserMenuItems';
|
||||
import { DocsUrl } from '../../common';
|
||||
import { DocsUrl } from '../../shared/common';
|
||||
import DarkModeSwitcher from '../components/DarkModeSwitcher';
|
||||
|
||||
export default function LandingPage() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DocsUrl, BlogUrl } from '../../common';
|
||||
import { DocsUrl, BlogUrl } from '../../shared/common';
|
||||
import daBoiAvatar from '../static/da-boi.png';
|
||||
import avatarPlaceholder from '../static/avatar-placeholder.png';
|
||||
import { routes } from 'wasp/client/router';
|
||||
|
@ -12,8 +12,8 @@ import {
|
||||
import { useState, useMemo } from 'react';
|
||||
import { CgSpinner } from 'react-icons/cg';
|
||||
import { TiDelete } from 'react-icons/ti';
|
||||
import type { GeneratedSchedule, MainTask, SubTask } from '../../gpt/schedule';
|
||||
import { cn } from '../cn';
|
||||
import type { GeneratedSchedule, MainTask, SubTask } from './schedule';
|
||||
import { cn } from '../client/cn';
|
||||
|
||||
export default function DemoAppPage() {
|
||||
return (
|
@ -1,12 +1,7 @@
|
||||
import { type User, type Task } from 'wasp/entities';
|
||||
import type { Task, GptResponse } from 'wasp/entities';
|
||||
import type { GenerateGptResponse, CreateTask, DeleteTask, UpdateTask, GetGptResponses, GetAllTasksByUser } from 'wasp/server/operations';
|
||||
import { HttpError } from 'wasp/server';
|
||||
import {
|
||||
type GenerateGptResponse,
|
||||
type CreateTask,
|
||||
type DeleteTask,
|
||||
type UpdateTask,
|
||||
} from 'wasp/server/operations';
|
||||
import { GeneratedSchedule } from '../gpt/schedule';
|
||||
import { GeneratedSchedule } from './schedule';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const openai = setupOpenAI();
|
||||
@ -17,6 +12,7 @@ function setupOpenAI() {
|
||||
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
}
|
||||
|
||||
//#region Actions
|
||||
type GptPayload = {
|
||||
hours: string;
|
||||
};
|
||||
@ -45,7 +41,12 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, GeneratedSched
|
||||
throw openai;
|
||||
}
|
||||
|
||||
if (!context.user.credits && (!context.user.subscriptionStatus || context.user.subscriptionStatus === 'deleted' || context.user.subscriptionStatus === 'past_due')) {
|
||||
if (
|
||||
!context.user.credits &&
|
||||
(!context.user.subscriptionStatus ||
|
||||
context.user.subscriptionStatus === 'deleted' ||
|
||||
context.user.subscriptionStatus === 'past_due')
|
||||
) {
|
||||
throw new HttpError(402, 'User has not paid or is out of credits');
|
||||
} else if (context.user.credits && !context.user.subscriptionStatus) {
|
||||
console.log('decrementing credits');
|
||||
@ -217,3 +218,35 @@ export const deleteTask: DeleteTask<Pick<Task, 'id'>, Task> = async ({ id }, con
|
||||
|
||||
return task;
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Queries
|
||||
export const getGptResponses: GetGptResponses<void, GptResponse[]> = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.GptResponse.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllTasksByUser: GetAllTasksByUser<void, Task[]> = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.Task.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
||||
//#endregion
|
@ -1,4 +1,4 @@
|
||||
import { type EmailChecker } from 'wasp/server/jobs';
|
||||
import { type SendNewsletter } from 'wasp/server/jobs';
|
||||
|
||||
import { type User } from 'wasp/entities';
|
||||
import { emailSender } from 'wasp/server/email';
|
||||
@ -22,7 +22,7 @@ const emailToSend: Email = {
|
||||
};
|
||||
|
||||
// you could use this function to send newsletters, expiration notices, etc.
|
||||
export const checkAndQueueEmails: EmailChecker<never, void> = async (_args, context) => {
|
||||
export const checkAndQueueNewsletterEmails: SendNewsletter<never, void> = async (_args, context) => {
|
||||
// e.g. you could send an offer email 2 weeks before their subscription expires
|
||||
const currentDate = new Date();
|
||||
const twoWeeksFromNow = new Date(currentDate.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||
@ -32,7 +32,7 @@ export const checkAndQueueEmails: EmailChecker<never, void> = async (_args, cont
|
||||
datePaid: {
|
||||
equals: twoWeeksFromNow,
|
||||
},
|
||||
sendEmail: true,
|
||||
sendNewsletter: true,
|
||||
},
|
||||
})) as User[];
|
||||
|
@ -7,7 +7,7 @@ import { stripe } from './stripeClient';
|
||||
import { paymentPlans, PaymentPlanId, SubscriptionStatus } from '../plans';
|
||||
import { updateUserStripePaymentDetails } from './paymentDetails';
|
||||
import { emailSender } from 'wasp/server/email';
|
||||
import { assertUnreachable } from '../../utils';
|
||||
import { assertUnreachable } from '../../shared/utils';
|
||||
import { requireNodeEnvVar } from '../../server/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -1,32 +0,0 @@
|
||||
import { type GptResponse, type Task } from 'wasp/entities';
|
||||
import { HttpError } from 'wasp/server';
|
||||
import { type GetGptResponses, type GetAllTasksByUser } from 'wasp/server/operations';
|
||||
|
||||
export const getGptResponses: GetGptResponses<void, GptResponse[]> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.GptResponse.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllTasksByUser: GetAllTasksByUser<void, Task[]> = async (_args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.Task.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
@ -35,7 +35,7 @@ function generateMockUserData(): MockUserData {
|
||||
createdAt,
|
||||
lastActiveTimestamp,
|
||||
isAdmin: false,
|
||||
sendEmail: false,
|
||||
sendNewsletter: false,
|
||||
credits,
|
||||
subscriptionStatus,
|
||||
stripeId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
|
||||
|
Loading…
x
Reference in New Issue
Block a user