migrate to v0.12

and cleanup new auth

Update main.wasp

cleanup
This commit is contained in:
vincanger 2024-02-09 16:50:09 -05:00
parent 604cf3aba0
commit 8ec12e257c
56 changed files with 9863 additions and 690 deletions

5
.gitignore vendored
View File

@ -2,5 +2,6 @@
*/.env.server
*/.env.client
*/.DS_Store
.DS_Store
*/migrations
*/node_modules
*/migrations
.DS_Store

View File

@ -1,10 +1,11 @@
app SaaSTemplate {
app OpenSaaS {
wasp: {
version: "^0.11.8"
version: "^0.12.0"
},
title: "My Open SaaS App",
head: [
"<meta property='og:type' content='website' />",
"<meta property='og:title' content='My Open SaaS App' />",
"<meta property='og:url' content='https://opensaas.sh' />",
"<meta property='og:description' content='I made a SaaS App. Buy my stuff.' />",
"<meta property='og:image' content='https://opensaas.sh/public-banner.png' />",
@ -22,43 +23,46 @@ app SaaSTemplate {
// 🔐 Auth out of the box! https://wasp-lang.dev/docs/auth/overview
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
usernameAndPassword: { // !IMPORTANT: this method is only suitable for dev/testing. Use social or email methods in production.
userSignupFields: import { getUsernameAndPasswordUserFields } from "@src/server/auth/setUsername.js",
},
// google: { // Guide for setting up Auth via Google https://wasp-lang.dev/docs/auth/social-auth/overview
// getUserFieldsFn: import { getUserFields } from "@server/auth/google.js",
// configFn: import { config } from "@server/auth/google.js",
// userSignupFields: import { getGoogleUserFields } from "@src/server/auth/setUsername.js",
// configFn: import { getGoogleAuthConfig } from "@src/server/auth/setUsername.js",
// },
// gitHub: {
// userSignupFields: import { getGitHubUserFields } from "@src/server/auth/setUsername.js",
// configFn: import { getGitHubAuthConfig } from "@src/server/auth/setUsername.js",
// },
// email: {
// fromField: {
// name: "Open SaaS App",
// // make sure this address is the same you registered your SendGrid or MailGun account with!
// email: "me@example.com"
// email: "vince@wasp-lang.dev"
// },
// emailVerification: {
// clientRoute: EmailVerificationRoute,
// getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
// getEmailContentFn: import { getVerificationEmailContent } from "@src/server/auth/email.js",
// },
// passwordReset: {
// clientRoute: PasswordResetRoute,
// getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
// getEmailContentFn: import { getPasswordResetEmailContent } from "@src/server/auth/email.js",
// },
// userSignupFields: import { getEmailUserFields } from "@src/server/auth/setUsername.js",
// },
},
signup: {
additionalFields: import setIsAdminViaEmailSignup from "@server/auth/setIsAdminViaEmailSignup.js",
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/demo-app",
},
db: {
system: PostgreSQL,
seeds: [
import { devSeedUsers } from "@server/scripts/usersSeed.js",
import { devSeedUsers } from "@src/server/scripts/usersSeed.js",
]
},
client: {
rootComponent: import App from "@client/App",
rootComponent: import App from "@src/client/App",
},
emailSender: {
provider: SendGrid,
@ -68,26 +72,6 @@ app SaaSTemplate {
email: "me@example.com"
},
},
// add your dependencies here. the quickest way to find the latest version is `npm view <package-name> version`
dependencies: [
("@headlessui/react", "1.7.13"),
("@tailwindcss/forms", "^0.5.3"),
("@tailwindcss/typography", "^0.5.7"),
("react-icons", "4.11.0"),
("node-fetch", "3.3.0"),
("stripe", "11.15.0"),
("react-hot-toast", "^2.4.1"),
("react-apexcharts", "^1.4.1"),
("apexcharts", "^3.41.0"),
("headlessui", "^0.0.0"),
("@faker-js/faker", "8.3.1"),
("@google-analytics/data", "4.1.0"),
("openai", "^4.24.1"),
("prettier", "3.1.1"),
("prettier-plugin-tailwindcss", "0.5.11"),
("zod", "3.22.4"),
("aws-sdk", "^2.1551.0")
],
}
/* 💽 Wasp defines DB entities via Prisma Database Models:
@ -97,14 +81,10 @@ app SaaSTemplate {
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
username String @unique
password String
username String? @unique
createdAt DateTime @default(now())
lastActiveTimestamp DateTime @default(now())
isAdmin Boolean @default(false)
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
stripeId String?
checkoutSessionId String?
hasPaid Boolean @default(false)
@ -114,22 +94,11 @@ entity User {=psl
datePaid DateTime?
credits Int @default(3)
gptResponses GptResponse[]
externalAuthAssociations SocialLogin[]
contactFormMessages ContactFormMessage[]
tasks Task[]
files File[]
psl=}
entity SocialLogin {=psl
id String @id @default(uuid())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}
entity GptResponse {=psl
id String @id @default(uuid())
content String
@ -208,166 +177,166 @@ psl=}
route LandingPageRoute { path: "/", to: LandingPage }
page LandingPage {
component: import LandingPage from "@client/landing-page/LandingPage"
component: import LandingPage from "@src/client/landing-page/LandingPage"
}
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/auth/LoginPage"
component: import Login from "@src/client/auth/LoginPage"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/auth/SignupPage"
component: import { Signup } from "@src/client/auth/SignupPage"
}
// route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
// page RequestPasswordResetPage {
// component: import { RequestPasswordReset } from "@client/auth/RequestPasswordReset",
// component: import { RequestPasswordReset } from "@src/client/auth/RequestPasswordReset",
// }
// route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
// page PasswordResetPage {
// component: import { PasswordReset } from "@client/auth/PasswordReset",
// component: import { PasswordReset } from "@src/client/auth/PasswordReset",
// }
// route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
// page EmailVerificationPage {
// component: import { EmailVerification } from "@client/auth/EmailVerification",
// component: import { EmailVerification } from "@src/client/auth/EmailVerification",
// }
route DemoAppRoute { path: "/demo-app", to: DemoAppPage }
page DemoAppPage {
authRequired: true,
component: import DemoAppPage from "@client/app/DemoAppPage"
component: import DemoAppPage from "@src/client/app/DemoAppPage"
}
route PricingPageRoute { path: "/pricing", to: PricingPage }
page PricingPage {
component: import PricingPage from "@client/app/PricingPage"
component: import PricingPage from "@src/client/app/PricingPage"
}
route AccountRoute { path: "/account", to: AccountPage }
page AccountPage {
authRequired: true,
component: import Account from "@client/app/AccountPage"
component: import Account from "@src/client/app/AccountPage"
}
route CheckoutRoute { path: "/checkout", to: CheckoutPage }
page CheckoutPage {
authRequired: true,
component: import Checkout from "@client/app/CheckoutPage"
component: import Checkout from "@src/client/app/CheckoutPage"
}
route FileUploadRoute { path: "/file-upload", to: FileUploadPage }
page FileUploadPage {
authRequired: true,
component: import FileUpload from "@client/app/FileUploadPage"
component: import FileUpload from "@src/client/app/FileUploadPage"
}
route AdminRoute { path: "/admin", to: DashboardPage }
page DashboardPage {
authRequired: true,
component: import Dashboard from "@client/admin/pages/DashboardPage"
component: import Dashboard from "@src/client/admin/pages/DashboardPage"
}
route AdminUsersRoute { path: "/admin/users", to: AdminUsersPage }
page AdminUsersPage {
authRequired: true,
component: import AdminUsers from "@client/admin/pages/Users"
component: import AdminUsers from "@src/client/admin/pages/Users"
}
route AdminSettingsRoute { path: "/admin/settings", to: AdminSettingsPage }
page AdminSettingsPage {
authRequired: true,
component: import AdminSettings from "@client/admin/pages/Settings"
component: import AdminSettings from "@src/client/admin/pages/Settings"
}
route AdminChartsRoute { path: "/admin/chart", to: AdminChartsPage }
page AdminChartsPage {
authRequired: true,
component: import AdminCharts from "@client/admin/pages/Chart"
component: import AdminCharts from "@src/client/admin/pages/Chart"
}
route AdminMessagesRoute { path: "/admin/messages", to: AdminMessagesPage }
page AdminMessagesPage {
authRequired: true,
component: import AdminMessages from "@client/admin/pages/Messages"
component: import AdminMessages from "@src/client/admin/pages/Messages"
}
route AdminFormElementsRoute { path: "/admin/forms/form-elements", to: AdminFormElementsPage }
page AdminFormElementsPage {
authRequired: true,
component: import AdminForms from "@client/admin/pages/Form/FormElements"
component: import AdminForms from "@src/client/admin/pages/Form/FormElements"
}
route AdminFormLayoutsRoute { path: "/admin/forms/form-layouts", to: AdminFormLayoutsPage }
page AdminFormLayoutsPage {
authRequired: true,
component: import AdminForms from "@client/admin/pages/Form/FormLayout"
component: import AdminForms from "@src/client/admin/pages/Form/FormLayout"
}
route AdminCalendarRoute { path: "/admin/calendar", to: AdminCalendarPage }
page AdminCalendarPage {
authRequired: true,
component: import AdminCalendar from "@client/admin/pages/Calendar"
component: import AdminCalendar from "@src/client/admin/pages/Calendar"
}
route AdminUIAlertsRoute { path: "/admin/ui/alerts", to: AdminUIAlertsPage }
page AdminUIAlertsPage {
authRequired: true,
component: import AdminUI from "@client/admin/pages/UiElements/Alerts"
component: import AdminUI from "@src/client/admin/pages/UiElements/Alerts"
}
route AdminUIButtonsRoute { path: "/admin/ui/buttons", to: AdminUIButtonsPage }
page AdminUIButtonsPage {
authRequired: true,
component: import AdminUI from "@client/admin/pages/UiElements/Buttons"
component: import AdminUI from "@src/client/admin/pages/UiElements/Buttons"
}
/* ⛑ These are the Wasp Operations, which allow the client and server to interact:
* https://wasp-lang.dev/docs/data-model/operations/overview
*/
// 📝 Actions aka Mutations
// 📝 Actions
action generateGptResponse {
fn: import { generateGptResponse } from "@server/actions.js",
fn: import { generateGptResponse } from "@src/server/actions.js",
entities: [User, Task, GptResponse]
}
action createTask {
fn: import { createTask } from "@server/actions.js",
fn: import { createTask } from "@src/server/actions.js",
entities: [Task]
}
action deleteTask {
fn: import { deleteTask } from "@server/actions.js",
fn: import { deleteTask } from "@src/server/actions.js",
entities: [Task]
}
action updateTask {
fn: import { updateTask } from "@server/actions.js",
fn: import { updateTask } from "@src/server/actions.js",
entities: [Task]
}
action stripePayment {
fn: import { stripePayment } from "@server/actions.js",
fn: import { stripePayment } from "@src/server/actions.js",
entities: [User]
}
action updateCurrentUser {
fn: import { updateCurrentUser } from "@server/actions.js",
fn: import { updateCurrentUser } from "@src/server/actions.js",
entities: [User]
}
action updateUserById {
fn: import { updateUserById } from "@server/actions.js",
fn: import { updateUserById } from "@src/server/actions.js",
entities: [User]
}
action createFile {
fn: import { createFile } from "@server/actions.js",
fn: import { createFile } from "@src/server/actions.js",
entities: [User, File]
}
@ -375,32 +344,32 @@ action createFile {
// 📚 Queries
query getGptResponses {
fn: import { getGptResponses } from "@server/queries.js",
fn: import { getGptResponses } from "@src/server/queries.js",
entities: [User, GptResponse]
}
query getAllTasksByUser {
fn: import { getAllTasksByUser } from "@server/queries.js",
fn: import { getAllTasksByUser } from "@src/server/queries.js",
entities: [Task]
}
query getAllFilesByUser {
fn: import { getAllFilesByUser } from "@server/queries.js",
fn: import { getAllFilesByUser } from "@src/server/queries.js",
entities: [User, File]
}
query getDownloadFileSignedURL {
fn: import { getDownloadFileSignedURL } from "@server/queries.js",
fn: import { getDownloadFileSignedURL } from "@src/server/queries.js",
entities: [User, File]
}
query getDailyStats {
fn: import { getDailyStats } from "@server/queries.js",
fn: import { getDailyStats } from "@src/server/queries.js",
entities: [User, DailyStats]
}
query getPaginatedUsers {
fn: import { getPaginatedUsers } from "@server/queries.js",
fn: import { getPaginatedUsers } from "@src/server/queries.js",
entities: [User]
}
@ -410,9 +379,9 @@ query getPaginatedUsers {
*/
api stripeWebhook {
fn: import { stripeWebhook } from "@server/webhooks/stripe.js",
fn: import { stripeWebhook } from "@src/server/webhooks/stripe.js",
entities: [User],
middlewareConfigFn: import { stripeMiddlewareFn } from "@server/webhooks/stripe.js",
middlewareConfigFn: import { stripeMiddlewareFn } from "@src/server/webhooks/stripe.js",
httpRoute: (POST, "/stripe-webhook")
}
@ -423,7 +392,7 @@ api stripeWebhook {
job emailChecker {
executor: PgBoss,
perform: {
fn: import { checkAndQueueEmails } from "@server/workers/checkAndQueueEmails.js"
fn: import { checkAndQueueEmails } from "@src/server/workers/checkAndQueueEmails.js"
},
schedule: {
cron: "0 7 * * 1" // at 7:00 am every Monday
@ -434,7 +403,7 @@ job emailChecker {
job dailyStatsJob {
executor: PgBoss,
perform: {
fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js"
fn: import { calculateDailyStats } from "@src/server/workers/calculateDailyStats.js"
},
schedule: {
cron: "0 * * * *" // every hour. useful in production

9380
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
app/package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "opensaas",
"dependencies": {
"@faker-js/faker": "8.3.1",
"@google-analytics/data": "4.1.0",
"@headlessui/react": "1.7.13",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.7",
"apexcharts": "^3.41.0",
"aws-sdk": "^2.1551.0",
"headlessui": "^0.0.0",
"node-fetch": "3.3.0",
"openai": "^4.24.1",
"prettier": "3.1.1",
"prettier-plugin-tailwindcss": "0.5.11",
"react": "^18.2.0",
"react-apexcharts": "^1.4.1",
"react-hot-toast": "^2.4.1",
"react-icons": "4.11.0",
"stripe": "11.15.0",
"wasp": "file:.wasp/out/sdk/wasp",
"zod": "3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/react": "^18.0.37",
"@types/react-router-dom": "^5.3.3",
"prisma": "4.16.2",
"typescript": "^5.1.0",
"vite": "^4.3.9"
}
}

0
app/public/.gitkeep Normal file
View File

BIN
app/public/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

View File

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

View File

@ -1,9 +1,9 @@
import { useAuth } from 'wasp/client/auth';
import { updateCurrentUser } from 'wasp/client/operations';
import './Main.css';
import AppNavBar from './components/AppNavBar';
import { useMemo, useEffect, ReactNode } from 'react';
import { useLocation } from 'react-router-dom';
import useAuth from '@wasp/auth/useAuth';
import updateCurrentUser from '@wasp/actions/updateCurrentUser';
/**
* use this component to wrap all child components
@ -43,16 +43,16 @@ export default function App({ children }: { children: ReactNode }) {
return (
<>
<div className='min-h-screen dark:text-white dark:bg-boxdark-2'>
{isAdminDashboard ? (
<>{children}</>
) : (
<>
{shouldDisplayAppNavBar && <AppNavBar />}
<div className='mx-auto max-w-7xl sm:px-6 lg:px-8'>{children}</div>
</>
)}
</div>
<div className='min-h-screen dark:text-white dark:bg-boxdark-2'>
{isAdminDashboard ? (
<>{children}</>
) : (
<>
{shouldDisplayAppNavBar && <AppNavBar />}
<div className='mx-auto max-w-7xl sm:px-6 lg:px-8'>{children}</div>
</>
)}
</div>
</>
);
}

View File

@ -1,3 +1,3 @@
import { DailyStats } from "@wasp/entities";
import { type DailyStats } from "wasp/entities";
export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?:DailyStats[], isLoading?: boolean }

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from 'react';
const DropdownDefault = () => {
const [dropdownOpen, setDropdownOpen] = useState(false);
@ -10,16 +10,11 @@ const DropdownDefault = () => {
useEffect(() => {
const clickHandler = ({ target }: MouseEvent) => {
if (!dropdown.current) return;
if (
!dropdownOpen ||
dropdown.current.contains(target) ||
trigger.current.contains(target)
)
return;
if (!dropdownOpen || dropdown.current.contains(target) || trigger.current.contains(target)) return;
setDropdownOpen(false);
};
document.addEventListener("click", clickHandler);
return () => document.removeEventListener("click", clickHandler);
document.addEventListener('click', clickHandler);
return () => document.removeEventListener('click', clickHandler);
});
// close if the esc key is pressed
@ -28,31 +23,25 @@ const DropdownDefault = () => {
if (!dropdownOpen || keyCode !== 27) return;
setDropdownOpen(false);
};
document.addEventListener("keydown", keyHandler);
return () => document.removeEventListener("keydown", keyHandler);
document.addEventListener('keydown', keyHandler);
return () => document.removeEventListener('keydown', keyHandler);
});
return (
<div className="relative">
<div className='relative'>
<button ref={trigger} onClick={() => setDropdownOpen(!dropdownOpen)}>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d="M2.25 11.25C3.49264 11.25 4.5 10.2426 4.5 9C4.5 7.75736 3.49264 6.75 2.25 6.75C1.00736 6.75 0 7.75736 0 9C0 10.2426 1.00736 11.25 2.25 11.25Z"
fill="#98A6AD"
d='M2.25 11.25C3.49264 11.25 4.5 10.2426 4.5 9C4.5 7.75736 3.49264 6.75 2.25 6.75C1.00736 6.75 0 7.75736 0 9C0 10.2426 1.00736 11.25 2.25 11.25Z'
fill='#98A6AD'
/>
<path
d="M9 11.25C10.2426 11.25 11.25 10.2426 11.25 9C11.25 7.75736 10.2426 6.75 9 6.75C7.75736 6.75 6.75 7.75736 6.75 9C6.75 10.2426 7.75736 11.25 9 11.25Z"
fill="#98A6AD"
d='M9 11.25C10.2426 11.25 11.25 10.2426 11.25 9C11.25 7.75736 10.2426 6.75 9 6.75C7.75736 6.75 6.75 7.75736 6.75 9C6.75 10.2426 7.75736 11.25 9 11.25Z'
fill='#98A6AD'
/>
<path
d="M15.75 11.25C16.9926 11.25 18 10.2426 18 9C18 7.75736 16.9926 6.75 15.75 6.75C14.5074 6.75 13.5 7.75736 13.5 9C13.5 10.2426 14.5074 11.25 15.75 11.25Z"
fill="#98A6AD"
d='M15.75 11.25C16.9926 11.25 18 10.2426 18 9C18 7.75736 16.9926 6.75 15.75 6.75C14.5074 6.75 13.5 7.75736 13.5 9C13.5 10.2426 14.5074 11.25 15.75 11.25Z'
fill='#98A6AD'
/>
</svg>
</button>
@ -61,56 +50,56 @@ const DropdownDefault = () => {
onFocus={() => setDropdownOpen(true)}
onBlur={() => setDropdownOpen(false)}
className={`absolute right-0 top-full z-40 w-40 space-y-1 rounded-sm border border-stroke bg-white p-1.5 shadow-default dark:border-strokedark dark:bg-boxdark ${
dropdownOpen === true ? "block" : "hidden"
dropdownOpen === true ? 'block' : 'hidden'
}`}
>
<button className="flex w-full items-center gap-2 rounded-sm py-1.5 px-4 text-left text-sm hover:bg-gray dark:hover:bg-meta-4">
<button className='flex w-full items-center gap-2 rounded-sm py-1.5 px-4 text-left text-sm hover:bg-gray dark:hover:bg-meta-4'>
<svg
className="fill-current"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className='fill-current'
width='16'
height='16'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath="url(#clip0_62_9787)">
<g clipPath='url(#clip0_62_9787)'>
<path
d="M15.55 2.97499C15.55 2.77499 15.475 2.57499 15.325 2.42499C15.025 2.12499 14.725 1.82499 14.45 1.52499C14.175 1.24999 13.925 0.974987 13.65 0.724987C13.525 0.574987 13.375 0.474986 13.175 0.449986C12.95 0.424986 12.75 0.474986 12.575 0.624987L10.875 2.32499H2.02495C1.17495 2.32499 0.449951 3.02499 0.449951 3.89999V14C0.449951 14.85 1.14995 15.575 2.02495 15.575H12.15C13 15.575 13.725 14.875 13.725 14V5.12499L15.35 3.49999C15.475 3.34999 15.55 3.17499 15.55 2.97499ZM8.19995 8.99999C8.17495 9.02499 8.17495 9.02499 8.14995 9.02499L6.34995 9.62499L6.94995 7.82499C6.94995 7.79999 6.97495 7.79999 6.97495 7.77499L11.475 3.27499L12.725 4.49999L8.19995 8.99999ZM12.575 14C12.575 14.25 12.375 14.45 12.125 14.45H2.02495C1.77495 14.45 1.57495 14.25 1.57495 14V3.87499C1.57495 3.62499 1.77495 3.42499 2.02495 3.42499H9.72495L6.17495 6.99999C6.04995 7.12499 5.92495 7.29999 5.87495 7.49999L4.94995 10.3C4.87495 10.5 4.92495 10.675 5.02495 10.85C5.09995 10.95 5.24995 11.1 5.52495 11.1H5.62495L8.49995 10.15C8.67495 10.1 8.84995 9.97499 8.97495 9.84999L12.575 6.24999V14ZM13.5 3.72499L12.25 2.49999L13.025 1.72499C13.225 1.92499 14.05 2.74999 14.25 2.97499L13.5 3.72499Z"
fill=""
d='M15.55 2.97499C15.55 2.77499 15.475 2.57499 15.325 2.42499C15.025 2.12499 14.725 1.82499 14.45 1.52499C14.175 1.24999 13.925 0.974987 13.65 0.724987C13.525 0.574987 13.375 0.474986 13.175 0.449986C12.95 0.424986 12.75 0.474986 12.575 0.624987L10.875 2.32499H2.02495C1.17495 2.32499 0.449951 3.02499 0.449951 3.89999V14C0.449951 14.85 1.14995 15.575 2.02495 15.575H12.15C13 15.575 13.725 14.875 13.725 14V5.12499L15.35 3.49999C15.475 3.34999 15.55 3.17499 15.55 2.97499ZM8.19995 8.99999C8.17495 9.02499 8.17495 9.02499 8.14995 9.02499L6.34995 9.62499L6.94995 7.82499C6.94995 7.79999 6.97495 7.79999 6.97495 7.77499L11.475 3.27499L12.725 4.49999L8.19995 8.99999ZM12.575 14C12.575 14.25 12.375 14.45 12.125 14.45H2.02495C1.77495 14.45 1.57495 14.25 1.57495 14V3.87499C1.57495 3.62499 1.77495 3.42499 2.02495 3.42499H9.72495L6.17495 6.99999C6.04995 7.12499 5.92495 7.29999 5.87495 7.49999L4.94995 10.3C4.87495 10.5 4.92495 10.675 5.02495 10.85C5.09995 10.95 5.24995 11.1 5.52495 11.1H5.62495L8.49995 10.15C8.67495 10.1 8.84995 9.97499 8.97495 9.84999L12.575 6.24999V14ZM13.5 3.72499L12.25 2.49999L13.025 1.72499C13.225 1.92499 14.05 2.74999 14.25 2.97499L13.5 3.72499Z'
fill=''
/>
</g>
<defs>
<clipPath id="clip0_62_9787">
<rect width="16" height="16" fill="white" />
<clipPath id='clip0_62_9787'>
<rect width='16' height='16' fill='white' />
</clipPath>
</defs>
</svg>
Edit
</button>
<button className="flex w-full items-center gap-2 rounded-sm py-1.5 px-4 text-left text-sm hover:bg-gray dark:hover:bg-meta-4">
<button className='flex w-full items-center gap-2 rounded-sm py-1.5 px-4 text-left text-sm hover:bg-gray dark:hover:bg-meta-4'>
<svg
className="fill-current"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className='fill-current'
width='16'
height='16'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d="M12.225 2.20005H10.3V1.77505C10.3 1.02505 9.70005 0.425049 8.95005 0.425049H7.02505C6.27505 0.425049 5.67505 1.02505 5.67505 1.77505V2.20005H3.75005C3.02505 2.20005 2.42505 2.80005 2.42505 3.52505V4.27505C2.42505 4.82505 2.75005 5.27505 3.22505 5.47505L3.62505 13.75C3.67505 14.775 4.52505 15.575 5.55005 15.575H10.4C11.425 15.575 12.275 14.775 12.325 13.75L12.75 5.45005C13.225 5.25005 13.55 4.77505 13.55 4.25005V3.50005C13.55 2.80005 12.95 2.20005 12.225 2.20005ZM6.82505 1.77505C6.82505 1.65005 6.92505 1.55005 7.05005 1.55005H8.97505C9.10005 1.55005 9.20005 1.65005 9.20005 1.77505V2.20005H6.85005V1.77505H6.82505ZM3.57505 3.52505C3.57505 3.42505 3.65005 3.32505 3.77505 3.32505H12.225C12.325 3.32505 12.425 3.40005 12.425 3.52505V4.27505C12.425 4.37505 12.35 4.47505 12.225 4.47505H3.77505C3.67505 4.47505 3.57505 4.40005 3.57505 4.27505V3.52505V3.52505ZM10.425 14.45H5.57505C5.15005 14.45 4.80005 14.125 4.77505 13.675L4.40005 5.57505H11.625L11.25 13.675C11.2 14.1 10.85 14.45 10.425 14.45Z"
fill=""
d='M12.225 2.20005H10.3V1.77505C10.3 1.02505 9.70005 0.425049 8.95005 0.425049H7.02505C6.27505 0.425049 5.67505 1.02505 5.67505 1.77505V2.20005H3.75005C3.02505 2.20005 2.42505 2.80005 2.42505 3.52505V4.27505C2.42505 4.82505 2.75005 5.27505 3.22505 5.47505L3.62505 13.75C3.67505 14.775 4.52505 15.575 5.55005 15.575H10.4C11.425 15.575 12.275 14.775 12.325 13.75L12.75 5.45005C13.225 5.25005 13.55 4.77505 13.55 4.25005V3.50005C13.55 2.80005 12.95 2.20005 12.225 2.20005ZM6.82505 1.77505C6.82505 1.65005 6.92505 1.55005 7.05005 1.55005H8.97505C9.10005 1.55005 9.20005 1.65005 9.20005 1.77505V2.20005H6.85005V1.77505H6.82505ZM3.57505 3.52505C3.57505 3.42505 3.65005 3.32505 3.77505 3.32505H12.225C12.325 3.32505 12.425 3.40005 12.425 3.52505V4.27505C12.425 4.37505 12.35 4.47505 12.225 4.47505H3.77505C3.67505 4.47505 3.57505 4.40005 3.57505 4.27505V3.52505V3.52505ZM10.425 14.45H5.57505C5.15005 14.45 4.80005 14.125 4.77505 13.675L4.40005 5.57505H11.625L11.25 13.675C11.2 14.1 10.85 14.45 10.425 14.45Z'
fill=''
/>
<path
d="M8.00005 8.1001C7.70005 8.1001 7.42505 8.3501 7.42505 8.6751V11.8501C7.42505 12.1501 7.67505 12.4251 8.00005 12.4251C8.30005 12.4251 8.57505 12.1751 8.57505 11.8501V8.6751C8.57505 8.3501 8.30005 8.1001 8.00005 8.1001Z"
fill=""
d='M8.00005 8.1001C7.70005 8.1001 7.42505 8.3501 7.42505 8.6751V11.8501C7.42505 12.1501 7.67505 12.4251 8.00005 12.4251C8.30005 12.4251 8.57505 12.1751 8.57505 11.8501V8.6751C8.57505 8.3501 8.30005 8.1001 8.00005 8.1001Z'
fill=''
/>
<path
d="M9.99994 8.60004C9.67494 8.57504 9.42494 8.80004 9.39994 9.12504L9.24994 11.325C9.22494 11.625 9.44994 11.9 9.77494 11.925C9.79994 11.925 9.79994 11.925 9.82494 11.925C10.1249 11.925 10.3749 11.7 10.3749 11.4L10.5249 9.20004C10.5249 8.87504 10.2999 8.62504 9.99994 8.60004Z"
fill=""
d='M9.99994 8.60004C9.67494 8.57504 9.42494 8.80004 9.39994 9.12504L9.24994 11.325C9.22494 11.625 9.44994 11.9 9.77494 11.925C9.79994 11.925 9.79994 11.925 9.82494 11.925C10.1249 11.925 10.3749 11.7 10.3749 11.4L10.5249 9.20004C10.5249 8.87504 10.2999 8.62504 9.99994 8.60004Z'
fill=''
/>
<path
d="M5.97497 8.60004C5.67497 8.62504 5.42497 8.90004 5.44997 9.20004L5.62497 11.4C5.64997 11.7 5.89997 11.925 6.17497 11.925C6.19997 11.925 6.19997 11.925 6.22497 11.925C6.52497 11.9 6.77497 11.625 6.74997 11.325L6.57497 9.12504C6.57497 8.80004 6.29997 8.57504 5.97497 8.60004Z"
fill=""
d='M5.97497 8.60004C5.67497 8.62504 5.42497 8.90004 5.44997 9.20004L5.62497 11.4C5.64997 11.7 5.89997 11.925 6.17497 11.925C6.19997 11.925 6.19997 11.925 6.22497 11.925C6.52497 11.9 6.77497 11.625 6.74997 11.325L6.57497 9.12504C6.57497 8.80004 6.29997 8.57504 5.97497 8.60004Z'
fill=''
/>
</svg>
Delete

View File

@ -1,7 +1,7 @@
import { type User } from 'wasp/entities';
import DarkModeSwitcher from './DarkModeSwitcher';
import MessageButton from './MessageButton';
import DropdownUser from '../../components/DropdownUser';
import type { User } from '@wasp/entities'
const Header = (props: {
sidebarOpen: string | boolean | undefined;
@ -12,7 +12,7 @@ const Header = (props: {
<header className='sticky top-0 z-999 flex w-full bg-white dark:bg-boxdark dark:drop-shadow-none'>
<div className='flex flex-grow items-center justify-between sm:justify-end sm:gap-5 px-8 py-5 shadow '>
<div className='flex items-center gap-2 sm:gap-4 lg:hidden'>
{/* <!-- Hamburger Toggle BTN --> */}
{/* <!-- Hamburger Toggle BTN --> */}
<button
aria-controls='sidebar'
@ -56,21 +56,19 @@ const Header = (props: {
</button>
{/* <!-- Hamburger Toggle BTN --> */}
</div>
<ul className='flex items-center gap-2 2xsm:gap-4'>
{/* <!-- Dark Mode Toggler --> */}
<DarkModeSwitcher />
{/* <!-- Dark Mode Toggler --> */}
<ul className='flex items-center gap-2 2xsm:gap-4'>
{/* <!-- Dark Mode Toggler --> */}
<DarkModeSwitcher />
{/* <!-- Dark Mode Toggler --> */}
{/* <!-- Chat Notification Area --> */}
<MessageButton />
{/* <!-- Chat Notification Area --> */}
</ul>
{/* <!-- Chat Notification Area --> */}
<MessageButton />
{/* <!-- Chat Notification Area --> */}
</ul>
<div className='flex items-center gap-3 2xsm:gap-7'>
{/* <!-- User Area --> */}
{!!props.user && <DropdownUser user={props.user} />}
{/* <!-- User Area --> */}

View File

@ -1,41 +1,40 @@
import { Link } from '@wasp/router';
import { Link } from 'wasp/client/router';
const MessageButton = () => {
return (
<li className="relative" x-data="{ dropdownOpen: false, notifying: true }">
<li className='relative' x-data='{ dropdownOpen: false, notifying: true }'>
<Link
className="relative flex h-8.5 w-8.5 items-center justify-center rounded-full border-[0.5px] border-stroke bg-gray hover:text-primary dark:border-strokedark dark:bg-meta-4 dark:text-white"
to="/admin/messages"
className='relative flex h-8.5 w-8.5 items-center justify-center rounded-full border-[0.5px] border-stroke bg-gray hover:text-primary dark:border-strokedark dark:bg-meta-4 dark:text-white'
to='/admin/messages'
>
<span className="absolute -top-0.5 -right-0.5 z-1 h-2 w-2 rounded-full bg-meta-1">
<span className='absolute -top-0.5 -right-0.5 z-1 h-2 w-2 rounded-full bg-meta-1'>
{/* TODO: only animate if there are new messages */}
<span className="absolute -z-1 inline-flex h-full w-full animate-ping rounded-full bg-meta-1 opacity-75"></span>
<span className='absolute -z-1 inline-flex h-full w-full animate-ping rounded-full bg-meta-1 opacity-75'></span>
</span>
<svg
className="fill-current duration-300 ease-in-out"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className='fill-current duration-300 ease-in-out'
width='18'
height='18'
viewBox='0 0 18 18'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d="M10.9688 1.57495H7.03135C3.43135 1.57495 0.506348 4.41558 0.506348 7.90308C0.506348 11.3906 2.75635 13.8375 8.26885 16.3125C8.40947 16.3687 8.52197 16.3968 8.6626 16.3968C8.85947 16.3968 9.02822 16.3406 9.19697 16.2281C9.47822 16.0593 9.64697 15.75 9.64697 15.4125V14.2031H10.9688C14.5688 14.2031 17.522 11.3625 17.522 7.87495C17.522 4.38745 14.5688 1.57495 10.9688 1.57495ZM10.9688 12.9937H9.3376C8.80322 12.9937 8.35322 13.4437 8.35322 13.9781V15.0187C3.6001 12.825 1.74385 10.8 1.74385 7.9312C1.74385 5.14683 4.10635 2.8687 7.03135 2.8687H10.9688C13.8657 2.8687 16.2563 5.14683 16.2563 7.9312C16.2563 10.7156 13.8657 12.9937 10.9688 12.9937Z"
fill=""
d='M10.9688 1.57495H7.03135C3.43135 1.57495 0.506348 4.41558 0.506348 7.90308C0.506348 11.3906 2.75635 13.8375 8.26885 16.3125C8.40947 16.3687 8.52197 16.3968 8.6626 16.3968C8.85947 16.3968 9.02822 16.3406 9.19697 16.2281C9.47822 16.0593 9.64697 15.75 9.64697 15.4125V14.2031H10.9688C14.5688 14.2031 17.522 11.3625 17.522 7.87495C17.522 4.38745 14.5688 1.57495 10.9688 1.57495ZM10.9688 12.9937H9.3376C8.80322 12.9937 8.35322 13.4437 8.35322 13.9781V15.0187C3.6001 12.825 1.74385 10.8 1.74385 7.9312C1.74385 5.14683 4.10635 2.8687 7.03135 2.8687H10.9688C13.8657 2.8687 16.2563 5.14683 16.2563 7.9312C16.2563 10.7156 13.8657 12.9937 10.9688 12.9937Z'
fill=''
/>
<path
d="M5.42812 7.28442C5.0625 7.28442 4.78125 7.56567 4.78125 7.9313C4.78125 8.29692 5.0625 8.57817 5.42812 8.57817C5.79375 8.57817 6.075 8.29692 6.075 7.9313C6.075 7.56567 5.79375 7.28442 5.42812 7.28442Z"
fill=""
d='M5.42812 7.28442C5.0625 7.28442 4.78125 7.56567 4.78125 7.9313C4.78125 8.29692 5.0625 8.57817 5.42812 8.57817C5.79375 8.57817 6.075 8.29692 6.075 7.9313C6.075 7.56567 5.79375 7.28442 5.42812 7.28442Z'
fill=''
/>
<path
d="M9.00015 7.28442C8.63452 7.28442 8.35327 7.56567 8.35327 7.9313C8.35327 8.29692 8.63452 8.57817 9.00015 8.57817C9.33765 8.57817 9.64702 8.29692 9.64702 7.9313C9.64702 7.56567 9.33765 7.28442 9.00015 7.28442Z"
fill=""
d='M9.00015 7.28442C8.63452 7.28442 8.35327 7.56567 8.35327 7.9313C8.35327 8.29692 8.63452 8.57817 9.00015 8.57817C9.33765 8.57817 9.64702 8.29692 9.64702 7.9313C9.64702 7.56567 9.33765 7.28442 9.00015 7.28442Z'
fill=''
/>
<path
d="M12.5719 7.28442C12.2063 7.28442 11.925 7.56567 11.925 7.9313C11.925 8.29692 12.2063 8.57817 12.5719 8.57817C12.9375 8.57817 13.2188 8.29692 13.2188 7.9313C13.2188 7.56567 12.9094 7.28442 12.5719 7.28442Z"
fill=""
d='M12.5719 7.28442C12.2063 7.28442 11.925 7.56567 11.925 7.9313C11.925 8.29692 12.2063 8.57817 12.5719 8.57817C12.9375 8.57817 13.2188 8.29692 13.2188 7.9313C13.2188 7.56567 12.9094 7.28442 12.5719 7.28442Z'
fill=''
/>
</svg>
</Link>

View File

@ -1,10 +1,8 @@
import { PageViewSource } from "@wasp/entities";
const SourcesTable = ({ sources } : { sources: PageViewSource[] | undefined }) => {
import { type PageViewSource } from 'wasp/entities';
const SourcesTable = ({ sources }: { sources: PageViewSource[] | undefined }) => {
return (
<div className='rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1'>
<h4 className='mb-6 text-xl font-semibold text-black dark:text-white'>Top Sources</h4>
<div className='flex flex-col'>
@ -20,8 +18,7 @@ const SourcesTable = ({ sources } : { sources: PageViewSource[] | undefined }) =
</div>
</div>
{sources &&
sources.length > 0 ?
{sources && sources.length > 0 ? (
sources.map((source) => (
<div className='grid grid-cols-3 border-b border-stroke dark:border-strokedark'>
<div className='flex items-center gap-3 p-2.5 xl:p-5'>
@ -36,11 +33,12 @@ const SourcesTable = ({ sources } : { sources: PageViewSource[] | undefined }) =
<p className='text-black dark:text-white'>--</p>
</div>
</div>
)) : (
<div className='flex items-center justify-center p-2.5 xl:p-5'>
<p className='text-black dark:text-white'>No data to display</p>
</div>
)}
))
) : (
<div className='flex items-center justify-center p-2.5 xl:p-5'>
<p className='text-black dark:text-white'>No data to display</p>
</div>
)}
</div>
</div>
);

View File

@ -1,7 +1,7 @@
import { type User } from 'wasp/entities';
import { useState } from 'react';
import { User } from '@wasp/entities';
const SwitcherOne = ({ user, updateUserById}: { user?: Partial<User>, updateUserById?: any}) => {
const SwitcherOne = ({ user, updateUserById }: { user?: Partial<User>; updateUserById?: any }) => {
const [enabled, setEnabled] = useState<boolean>(user?.hasPaid || false);
return (

View File

@ -1,8 +1,6 @@
import { updateUserById, useQuery, getPaginatedUsers } from 'wasp/client/operations';
import { useState, useEffect } from 'react';
import SwitcherOne from './SwitcherOne';
import { useQuery } from '@wasp/queries';
import getPaginatedUsers from '@wasp/queries/getPaginatedUsers';
import updateUserById from '@wasp/actions/updateUserById';
import Loader from '../common/Loader';
import DropdownEditDelete from './DropdownEditDelete';
@ -208,7 +206,9 @@ const UsersTable = () => {
</div>
<div className='col-span-3 hidden items-center sm:flex'>
<p className='text-sm text-black dark:text-white'>{user.lastActiveTimestamp.toLocaleDateString() + ' ' + user.lastActiveTimestamp.toLocaleTimeString()}</p>
<p className='text-sm text-black dark:text-white'>
{user.lastActiveTimestamp.toLocaleDateString() + ' ' + user.lastActiveTimestamp.toLocaleTimeString()}
</p>
</div>
<div className='col-span-2 flex items-center'>
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>

View File

@ -1,7 +1,7 @@
import { useAuth } from "wasp/client/auth";
import { useState, ReactNode, FC } from 'react';
import Header from '../components/Header';
import Sidebar from '../components/Sidebar';
import useAuth from '@wasp/auth/useAuth';
interface Props {
children?: ReactNode;

View File

@ -1,3 +1,5 @@
import { type User } from 'wasp/entities';
import { useQuery, getDailyStats } from 'wasp/client/operations';
import TotalSignupsCard from '../components/TotalSignupsCard';
import TotalPageViewsCard from '../components/TotalPaidViewsCard';
import TotalPayingUsersCard from '../components/TotalPayingUsersCard';
@ -5,10 +7,7 @@ import TotalRevenueCard from '../components/TotalRevenueCard';
import RevenueAndProfitChart from '../components/RevenueAndProfitChart';
import SourcesTable from '../components/SourcesTable';
import DefaultLayout from '../layout/DefaultLayout';
import { useQuery } from '@wasp/queries';
import getDailyStats from '@wasp/queries/getDailyStats';
import { useHistory } from 'react-router-dom';
import type { User } from '@wasp/entities';
const Dashboard = ({ user }: { user: User }) => {
const history = useHistory();

View File

@ -1,8 +1,8 @@
import { User } from '@wasp/entities';
import logout from '@wasp/auth/logout';
import { Link } from '@wasp/router';
import { STRIPE_CUSTOMER_PORTAL_LINK } from '@wasp/shared/constants';
import { TierIds } from '@wasp/shared/constants';
import { Link } from 'wasp/client/router';
import { type User } from 'wasp/entities';
import { logout } from 'wasp/client/auth';
import { STRIPE_CUSTOMER_PORTAL_LINK } from '../../shared/constants';
import { TierIds } from '../../shared/constants';
export default function AccountPage({ user }: { user: User }) {
return (

View File

@ -1,11 +1,15 @@
import { type Task } from 'wasp/entities';
import {
generateGptResponse,
deleteTask,
updateTask,
createTask,
useQuery,
getAllTasksByUser,
} from 'wasp/client/operations';
import { useState, useEffect, useMemo } from 'react';
import generateGptResponse from '@wasp/actions/generateGptResponse';
import deleteTask from '@wasp/actions/deleteTask';
import updateTask from '@wasp/actions/updateTask';
import createTask from '@wasp/actions/createTask';
import { useQuery } from '@wasp/queries';
import getAllTasksByUser from '@wasp/queries/getAllTasksByUser';
import { Task } from '@wasp/entities';
import { CgSpinner } from 'react-icons/cg';
import { TiDelete } from 'react-icons/ti';
@ -15,11 +19,12 @@ export default function DemoAppPage() {
<div className='mx-auto max-w-7xl px-6 lg:px-8'>
<div className='mx-auto max-w-4xl text-center'>
<h2 className='mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl dark:text-white'>
<span className='text-yellow-500'>AI</span> Day Scheduler
<span className='text-yellow-500'>AI</span> Day Scheduler
</h2>
</div>
<p className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-white'>
This example app uses OpenAI's chat completions with function calling to return a structured JSON object. Try it out, enter your day's tasks, and let AI do the rest!
This example app uses OpenAI's chat completions with function calling to return a structured JSON object. Try
it out, enter your day's tasks, and let AI do the rest!
</p>
{/* begin AI-powered Todo List */}
<div className='my-8 border rounded-3xl border-gray-900/10'>
@ -36,9 +41,7 @@ export default function DemoAppPage() {
type TodoProps = Pick<Task, 'id' | 'isDone' | 'description' | 'time'>;
function Todo({ id, isDone, description, time }: TodoProps) {
const handleCheckboxChange = async (
e: React.ChangeEvent<HTMLInputElement>
) => {
const handleCheckboxChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
await updateTask({
id,
isDone: e.currentTarget.checked,
@ -66,13 +69,7 @@ function Todo({ id, isDone, description, time }: TodoProps) {
checked={isDone}
onChange={handleCheckboxChange}
/>
<span
className={`text-slate-600 ${
isDone ? 'line-through text-slate-500' : ''
}`}
>
{description}
</span>
<span className={`text-slate-600 ${isDone ? 'line-through text-slate-500' : ''}`}>{description}</span>
</div>
<div className='flex items-center gap-2'>
<input
@ -86,13 +83,7 @@ function Todo({ id, isDone, description, time }: TodoProps) {
value={time}
onChange={handleTimeChange}
/>
<span
className={`italic text-slate-600 text-xs ${
isDone ? 'text-slate-500' : ''
}`}
>
hrs
</span>
<span className={`italic text-slate-600 text-xs ${isDone ? 'text-slate-500' : ''}`}>hrs</span>
</div>
</div>
<div className='flex items-center justify-end w-15'>
@ -104,18 +95,13 @@ function Todo({ id, isDone, description, time }: TodoProps) {
);
}
function NewTaskForm({
handleCreateTask,
}: {
handleCreateTask: typeof createTask;
}) {
function NewTaskForm({ handleCreateTask }: { handleCreateTask: typeof createTask }) {
const [description, setDescription] = useState<string>('');
const [todaysHours, setTodaysHours] = useState<string>('8');
const [response, setResponse] = useState<any>(null);
const [isPlanGenerating, setIsPlanGenerating] = useState<boolean>(false);
const { data: tasks, isLoading: isTasksLoading } =
useQuery(getAllTasksByUser);
const { data: tasks, isLoading: isTasksLoading } = useQuery(getAllTasksByUser);
useEffect(() => {
console.log('response', response);
@ -179,20 +165,11 @@ function NewTaskForm({
{tasks!! && tasks.length > 0 ? (
<div className='space-y-4'>
{tasks.map((task: Task) => (
<Todo
key={task.id}
id={task.id}
isDone={task.isDone}
description={task.description}
time={task.time}
/>
<Todo key={task.id} id={task.id} isDone={task.isDone} description={task.description} time={task.time} />
))}
<div className='flex flex-col gap-3'>
<div className='flex items-center justify-between gap-3'>
<label
htmlFor='time'
className='text-sm text-gray-600 dark:text-gray-300 text-nowrap font-semibold'
>
<label htmlFor='time' className='text-sm text-gray-600 dark:text-gray-300 text-nowrap font-semibold'>
How many hours will you work today?
</label>
<input
@ -231,9 +208,7 @@ function NewTaskForm({
{!!response && (
<div className='flex flex-col'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-white'>
Today's Schedule
</h3>
<h3 className='text-lg font-semibold text-gray-900 dark:text-white'>Today's Schedule</h3>
<TaskTable schedule={response.schedule} />
</div>
@ -254,18 +229,11 @@ function TaskTable({ schedule }: { schedule: any[] }) {
<tr>
<th
className={`flex items-center justify-between gap-5 py-4 px-3 text-slate-800 border rounded-md border-slate-200 ${
task.priority === 'high'
? 'bg-red-50'
: task.priority === 'low'
? 'bg-green-50'
: 'bg-yellow-50'
task.priority === 'high' ? 'bg-red-50' : task.priority === 'low' ? 'bg-green-50' : 'bg-yellow-50'
}`}
>
<span>{task.name}</span>
<span className='opacity-70 text-xs font-medium italic'>
{' '}
{task.priority} priority
</span>
<span className='opacity-70 text-xs font-medium italic'> {task.priority} priority</span>
</th>
</tr>
</thead>
@ -275,10 +243,7 @@ function TaskTable({ schedule }: { schedule: any[] }) {
<td
className={`flex items-center justify-between py-2 px-3 text-slate-600 border rounded-md border-purple-100 bg-purple-50`}
>
<Subtask
description={subtask.description}
time={subtask.time}
/>
<Subtask description={subtask.description} time={subtask.time} />
</td>
</tr>
))}
@ -288,10 +253,7 @@ function TaskTable({ schedule }: { schedule: any[] }) {
<td
className={`flex items-center justify-between py-2 px-3 text-slate-600 border rounded-md border-purple-100 bg-purple-50`}
>
<Subtask
description={breakItem.description}
time={breakItem.time}
/>
<Subtask description={breakItem.description} time={breakItem.time} />
</td>
</tr>
))}
@ -309,9 +271,7 @@ function Subtask({ description, time }: { description: string; time: number }) {
if (time === 0) return 0;
const hours = Math.floor(time);
const minutes = Math.round((time - hours) * 60);
return `${hours > 0 ? hours + 'hr' : ''} ${
minutes > 0 ? minutes + 'min' : ''
}`;
return `${hours > 0 ? hours + 'hr' : ''} ${minutes > 0 ? minutes + 'min' : ''}`;
};
const minutes = useMemo(() => convertHrsToMinutes(time), [time]);
@ -324,20 +284,8 @@ function Subtask({ description, time }: { description: string; time: number }) {
checked={isDone}
onChange={(e) => setIsDone(e.currentTarget.checked)}
/>
<span
className={`text-slate-600 ${
isDone ? 'line-through text-slate-500 opacity-50' : ''
}`}
>
{description}
</span>
<span
className={`text-slate-600 ${
isDone ? 'line-through text-slate-500 opacity-50' : ''
}`}
>
{minutes}
</span>
<span className={`text-slate-600 ${isDone ? 'line-through text-slate-500 opacity-50' : ''}`}>{description}</span>
<span className={`text-slate-600 ${isDone ? 'line-through text-slate-500 opacity-50' : ''}`}>{minutes}</span>
</>
);
}

View File

@ -1,9 +1,6 @@
import { createFile, useQuery, getAllFilesByUser, getDownloadFileSignedURL } from 'wasp/client/operations';
import axios from 'axios';
import { useQuery } from '@wasp/queries';
import { useState, useEffect, FormEvent } from 'react';
import getAllFilesByUser from '@wasp/queries/getAllFilesByUser';
import createFile from '@wasp/actions/createFile';
import getDownloadFileSignedURL from '@wasp/queries/getDownloadFileSignedURL';
export default function FileUploadPage() {
const [fileToDownload, setFileToDownload] = useState<string>('');
@ -74,8 +71,8 @@ export default function FileUploadPage() {
</h2>
</div>
<p className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-white'>
This is an example file upload page using AWS S3. Maybe your app needs this. Maybe it
doesn't. But a lot of people asked for this feature, so here you go 🤝
This is an example file upload page using AWS S3. Maybe your app needs this. Maybe it doesn't. But a lot of
people asked for this feature, so here you go 🤝
</p>
<div className='my-8 border rounded-3xl border-gray-900/10'>
<div className='space-y-10 my-10 py-8 px-4 mx-auto sm:max-w-lg'>

View File

@ -1,8 +1,8 @@
import { TierIds, STRIPE_CUSTOMER_PORTAL_LINK } from '@wasp/shared/constants';
import { useAuth } from 'wasp/client/auth';
import { stripePayment } from 'wasp/client/operations';
import { TierIds, STRIPE_CUSTOMER_PORTAL_LINK } from '../../shared/constants';
import { AiFillCheckCircle } from 'react-icons/ai';
import { useState } from 'react';
import stripePayment from '@wasp/actions/stripePayment';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
export const tiers = [
@ -33,28 +33,28 @@ export const tiers = [
const PricingPage = () => {
const [isStripePaymentLoading, setIsStripePaymentLoading] = useState<boolean | string>(false);
const { data: user, isLoading: isUserLoading } = useAuth();
const { data: user, isLoading: isUserLoading } = useAuth();
const history = useHistory();
const history = useHistory();
async function handleBuyNowClick(tierId: string) {
if (!user) {
history.push('/login');
return;
}
try {
setIsStripePaymentLoading(tierId);
let stripeResults = await stripePayment(tierId);
if (stripeResults?.sessionUrl) {
window.open(stripeResults.sessionUrl, '_self');
}
} catch (error: any) {
console.error(error?.message ?? 'Something went wrong.');
} finally {
setIsStripePaymentLoading(false);
}
async function handleBuyNowClick(tierId: string) {
if (!user) {
history.push('/login');
return;
}
try {
setIsStripePaymentLoading(tierId);
let stripeResults = await stripePayment(tierId);
if (stripeResults?.sessionUrl) {
window.open(stripeResults.sessionUrl, '_self');
}
} catch (error: any) {
console.error(error?.message ?? 'Something went wrong.');
} finally {
setIsStripePaymentLoading(false);
}
}
return (
<div className='py-10 lg:mt-10'>
@ -95,7 +95,9 @@ const PricingPage = () => {
</div>
<p className='mt-4 text-sm leading-6 text-gray-600 dark:text-white'>{tier.description}</p>
<p className='mt-6 flex items-baseline gap-x-1 dark:text-white'>
<span className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>{tier.priceMonthly}</span>
<span className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>
{tier.priceMonthly}
</span>
<span className='text-sm font-semibold leading-6 text-gray-600 dark:text-white'>/month</span>
</p>
<ul role='list' className='mt-8 space-y-3 text-sm leading-6 text-gray-600 dark:text-white'>

View File

@ -1,5 +1,5 @@
// import { VerifyEmailForm } from "wasp/client/auth";
// import { Link } from 'react-router-dom';
// import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail';
// import { AuthWrapper } from './authWrapper';
// export function EmailVerification() {

View File

@ -1,9 +1,8 @@
import { useAuth, LoginForm } from 'wasp/client/auth';
import { useHistory } from 'react-router-dom';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { LoginForm } from '@wasp/auth/forms/Login';
import { AuthWrapper } from './authWrapper';
import useAuth from '@wasp/auth/useAuth';
export default function Login() {
const history = useHistory();

View File

@ -1,5 +1,5 @@
// import { ResetPasswordForm } from "wasp/client/auth";
// import { Link } from 'react-router-dom';
// import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword';
// import { AuthWrapper } from './authWrapper';
// export function PasswordReset() {

View File

@ -1,4 +1,4 @@
// import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword';
// import { ForgotPasswordForm } from "wasp/client/auth";
// import { AuthWrapper } from './authWrapper';
// export function RequestPasswordReset() {

View File

@ -1,5 +1,5 @@
import { SignupForm } from 'wasp/client/auth';
import { Link } from 'react-router-dom';
import { SignupForm } from '@wasp/auth/forms/Signup';
import { AuthWrapper } from './authWrapper';
export function Signup() {

View File

@ -1,27 +1,25 @@
import { Link } from 'wasp/client/router';
import { useAuth } from 'wasp/client/auth';
import { useState } from 'react';
import { Dialog } from '@headlessui/react';
import { BiLogIn } from 'react-icons/bi';
import { AiFillCloseCircle } from 'react-icons/ai';
import { HiBars3 } from 'react-icons/hi2';
import useAuth from '@wasp/auth/useAuth';
import logo from '../static/logo.png';
import DropdownUser from './DropdownUser';
import { DOCS_URL, BLOG_URL } from '@wasp/shared/constants';
import { DOCS_URL, BLOG_URL } from '../../shared/constants';
import DarkModeSwitcher from '../admin/components/DarkModeSwitcher';
import { UserMenuItems } from '../components/UserMenuItems';
import { Link } from '@wasp/router';
const navigation = [
{ name: 'AI Scheduler (Demo App)', href: '/demo-app' },
{ name: 'File Upload (AWS S3)', href: '/file-upload'},
{ name: 'File Upload (AWS S3)', href: '/file-upload' },
{ name: 'Pricing', href: '/pricing' },
{ name: 'Documentation', href: DOCS_URL },
{ name: 'Blog', href: BLOG_URL },
];
const NavLogo = () => (
<img className='h-8 w-8' src={logo} alt='Your SaaS App' />
);
const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />;
export default function AppNavBar() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@ -29,10 +27,7 @@ export default function AppNavBar() {
const { data: user, isLoading: isUserLoading } = useAuth();
return (
<header className='absolute inset-x-0 top-0 z-50 shadow sticky bg-white bg-opacity-50 backdrop-blur-lg backdrop-filter dark:border-strokedark dark:bg-boxdark-2'>
<nav
className='flex items-center justify-between p-6 lg:px-8'
aria-label='Global'
>
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex lg:flex-1'>
<a href='/' className='-m-1.5 p-1.5'>
<img className='h-8 w-8' src={logo} alt='My SaaS App' />
@ -65,10 +60,7 @@ export default function AppNavBar() {
</ul>
{isUserLoading ? null : !user ? (
<a
href={!user ? '/login' : '/account'}
className='text-sm font-semibold leading-6 ml-4'
>
<a href={!user ? '/login' : '/account'} className='text-sm font-semibold leading-6 ml-4'>
<div className='flex items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
</div>
@ -80,12 +72,7 @@ export default function AppNavBar() {
)}
</div>
</nav>
<Dialog
as='div'
className='lg:hidden'
open={mobileMenuOpen}
onClose={setMobileMenuOpen}
>
<Dialog as='div' className='lg:hidden' open={mobileMenuOpen} onClose={setMobileMenuOpen}>
<div className='fixed inset-0 z-50' />
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white dark:text-white dark:bg-boxdark px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10'>
<div className='flex items-center justify-between'>
@ -124,7 +111,7 @@ export default function AppNavBar() {
</div>
</Link>
) : (
<UserMenuItems user={user} setMobileMenuOpen={setMobileMenuOpen}/>
<UserMenuItems user={user} setMobileMenuOpen={setMobileMenuOpen} />
)}
</div>
</div>

View File

@ -1,9 +1,9 @@
import { type User } from 'wasp/entities';
import { useEffect, useRef, useState } from 'react';
import { CgProfile } from 'react-icons/cg';
import type { User } from '@wasp/entities'
import { UserMenuItems } from './UserMenuItems';
const DropdownUser = ({ user } : { user: Partial<User> }) => {
const DropdownUser = ({ user }: { user: Partial<User> }) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const trigger = useRef<any>(null);
@ -13,7 +13,7 @@ const DropdownUser = ({ user } : { user: Partial<User> }) => {
useEffect(() => {
const clickHandler = ({ target }: MouseEvent) => {
if (!dropdown.current) return
if (!dropdown.current) return;
if (!dropdownOpen || dropdown.current.contains(target) || trigger.current.contains(target)) {
return;
}

View File

@ -1,21 +1,15 @@
import { Link } from '@wasp/router';
import { Link } from 'wasp/client/router';
import { type User } from 'wasp/entities';
import { logout } from 'wasp/client/auth';
import { MdOutlineSpaceDashboard } from 'react-icons/md';
import { TfiDashboard } from 'react-icons/tfi';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';
export const UserMenuItems = ({
user,
setMobileMenuOpen,
}: {
user?: Partial<User>;
setMobileMenuOpen?: any;
}) => {
export const UserMenuItems = ({ user, setMobileMenuOpen }: { user?: Partial<User>; setMobileMenuOpen?: any }) => {
const path = window.location.pathname;
const handleMobileMenuClick = () => {
if (setMobileMenuOpen) setMobileMenuOpen(false);
}
};
return (
<>

View File

@ -1,22 +1,16 @@
import { Link } from 'wasp/client/router';
import { useAuth } from 'wasp/client/auth';
import { useState } from 'react';
import { Dialog } from '@headlessui/react';
import { AiFillCloseCircle } from 'react-icons/ai';
import { HiBars3 } from 'react-icons/hi2';
import { BiLogIn } from 'react-icons/bi';
import { Link } from '@wasp/router';
import logo from '../static/logo.png';
import openSaasBanner from '../static/open-saas-banner.png';
import {
features,
navigation,
faqs,
footerNavigation,
testimonials,
} from './contentSections';
import { features, navigation, faqs, footerNavigation, testimonials } from './contentSections';
import DropdownUser from '../components/DropdownUser';
import { DOCS_URL } from '@wasp/shared/constants';
import { DOCS_URL } from '../../shared/constants';
import { UserMenuItems } from '../components/UserMenuItems';
import useAuth from '@wasp/auth/useAuth';
import DarkModeSwitcher from '../admin/components/DarkModeSwitcher';
export default function LandingPage() {
@ -24,27 +18,20 @@ export default function LandingPage() {
const { data: user, isLoading: isUserLoading } = useAuth();
const NavLogo = () => (
<img className='h-8 w-8' src={logo} alt='Your SaaS App' />
);
const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />;
return (
<div className='bg-white dark:text-white dark:bg-boxdark-2'>
{/* Header */}
<header className='absolute inset-x-0 top-0 z-50 dark:bg-boxdark-2'>
<nav
className='flex items-center justify-between p-6 lg:px-8'
aria-label='Global'
>
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex items-center lg:flex-1'>
<a
href='/'
className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
>
<NavLogo />
<span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>
Your Saas
</span>
<span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Your Saas</span>
</a>
</div>
<div className='flex lg:hidden'>
@ -86,12 +73,7 @@ export default function LandingPage() {
</div>
</div>
</nav>
<Dialog
as='div'
className='lg:hidden'
open={mobileMenuOpen}
onClose={setMobileMenuOpen}
>
<Dialog as='div' className='lg:hidden' open={mobileMenuOpen} onClose={setMobileMenuOpen}>
<div className='fixed inset-0 z-50' />
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-boxdark dark:text-white'>
<div className='flex items-center justify-between'>
@ -149,8 +131,7 @@ export default function LandingPage() {
<div
className='aspect-[1020/880] w-[55rem] flex-none sm:right-1/4 sm:translate-x-1/2 dark:hidden bg-gradient-to-tr from-amber-400 to-purple-300 opacity-40'
style={{
clipPath:
'polygon(80% 20%, 90% 55%, 50% 100%, 70% 30%, 20% 50%, 50% 0)',
clipPath: 'polygon(80% 20%, 90% 55%, 50% 100%, 70% 30%, 20% 50%, 50% 0)',
}}
/>
</div>
@ -169,8 +150,7 @@ export default function LandingPage() {
<div className='mx-auto max-w-8xl px-6 lg:px-8'>
<div className='lg:mb-18 mx-auto max-w-3xl text-center'>
<h1 className='text-4xl font-bold text-gray-900 sm:text-6xl dark:text-white'>
Some <span className='italic'>cool</span> words about your
product
Some <span className='italic'>cool</span> words about your product
</h1>
<p className='mt-6 mx-auto max-w-2xl text-lg leading-8 text-gray-600 dark:text-white'>
With some more exciting words about your product!
@ -232,12 +212,7 @@ export default function LandingPage() {
</svg>
</div>
<div className='flex justify-center col-span-1 max-h-12 w-full object-contain dark:opacity-80'>
<svg
width={48}
height={48}
viewBox='0 0 32 32'
xmlns='http://www.w3.org/2000/svg'
>
<svg width={48} height={48} viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
<path
className='dark:fill-white'
fill='#545454'
@ -260,11 +235,7 @@ export default function LandingPage() {
</svg>
</div>
<div className='flex justify-center col-span-1 w-full max-h-12 object-contain dark:opacity-80'>
<svg
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid'
viewBox='0 0 256 260'
>
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMidYMid' viewBox='0 0 256 260'>
<path
className='dark:fill-white'
fill='#545454'
@ -296,9 +267,7 @@ export default function LandingPage() {
</div>
{feature.name}
</dt>
<dd className='mt-2 text-base leading-7 text-gray-600 dark:text-white'>
{feature.description}
</dd>
<dd className='mt-2 text-base leading-7 text-gray-600 dark:text-white'>{feature.description}</dd>
</div>
))}
</dl>
@ -319,18 +288,10 @@ export default function LandingPage() {
<p>{testimonial.quote}</p>
</blockquote>
<figcaption className='mt-6 text-base text-white'>
<a
href={testimonial.socialUrl}
className='flex items-center gap-x-2'
>
<img
src={testimonial.avatarSrc}
className='h-12 w-12 rounded-full'
/>
<a href={testimonial.socialUrl} className='flex items-center gap-x-2'>
<img src={testimonial.avatarSrc} className='h-12 w-12 rounded-full' />
<div>
<div className='font-semibold hover:underline'>
{testimonial.name}
</div>
<div className='font-semibold hover:underline'>{testimonial.name}</div>
<div className='mt-1'>{testimonial.role}</div>
</div>
</a>
@ -349,22 +310,14 @@ export default function LandingPage() {
</h2>
<dl className='mt-10 space-y-8 divide-y divide-gray-900/10'>
{faqs.map((faq) => (
<div
key={faq.id}
className='pt-8 lg:grid lg:grid-cols-12 lg:gap-8'
>
<div key={faq.id} className='pt-8 lg:grid lg:grid-cols-12 lg:gap-8'>
<dt className='text-base font-semibold leading-7 text-gray-900 lg:col-span-5 dark:text-white'>
{faq.question}
</dt>
<dd className='flex items-center justify-start gap-2 mt-4 lg:col-span-7 lg:mt-0'>
<p className='text-base leading-7 text-gray-600 dark:text-white'>
{faq.answer}
</p>
<p className='text-base leading-7 text-gray-600 dark:text-white'>{faq.answer}</p>
{faq.href && (
<a
href={faq.href}
className='text-base leading-7 text-yellow-500 hover:text-yellow-600'
>
<a href={faq.href} className='text-base leading-7 text-yellow-500 hover:text-yellow-600'>
Learn more
</a>
)}
@ -386,16 +339,11 @@ export default function LandingPage() {
</h2>
<div className='flex items-start justify-end mt-10 gap-20'>
<div>
<h3 className='text-sm font-semibold leading-6 text-gray-900 dark:text-white'>
App
</h3>
<h3 className='text-sm font-semibold leading-6 text-gray-900 dark:text-white'>App</h3>
<ul role='list' className='mt-6 space-y-4'>
{footerNavigation.app.map((item) => (
<li key={item.name}>
<a
href={item.href}
className='text-sm leading-6 text-gray-600 hover:text-gray-900 dark:text-white'
>
<a href={item.href} className='text-sm leading-6 text-gray-600 hover:text-gray-900 dark:text-white'>
{item.name}
</a>
</li>
@ -403,16 +351,11 @@ export default function LandingPage() {
</ul>
</div>
<div>
<h3 className='text-sm font-semibold leading-6 text-gray-900 dark:text-white'>
Company
</h3>
<h3 className='text-sm font-semibold leading-6 text-gray-900 dark:text-white'>Company</h3>
<ul role='list' className='mt-6 space-y-4'>
{footerNavigation.company.map((item) => (
<li key={item.name}>
<a
href={item.href}
className='text-sm leading-6 text-gray-600 hover:text-gray-900 dark:text-white'
>
<a href={item.href} className='text-sm leading-6 text-gray-600 hover:text-gray-900 dark:text-white'>
{item.name}
</a>
</li>

View File

@ -1,4 +1,4 @@
import { DOCS_URL, BLOG_URL } from '@wasp/shared/constants';
import { DOCS_URL, BLOG_URL } from '../../shared/constants';
import daBoiAvatar from '../static/da-boi.png';
import avatarPlaceholder from '../static/avatar-placeholder.png';
@ -35,7 +35,6 @@ export const features = [
},
];
export const testimonials = [
{
name: 'Da Boi',
role: 'Wasp Mascot',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

View File

@ -1,55 +0,0 @@
// =============================== 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"
],
}

View File

@ -1 +0,0 @@
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />

View File

@ -1,26 +1,33 @@
import { type User, type Task, type File } from 'wasp/entities';
import { HttpError } from 'wasp/server';
import {
type GenerateGptResponse,
type StripePayment,
type UpdateCurrentUser,
type UpdateUserById,
type CreateTask,
type DeleteTask,
type UpdateTask,
type CreateFile,
} from 'wasp/server/operations';
import Stripe from 'stripe';
import fetch from 'node-fetch';
import HttpError from '@wasp/core/HttpError.js';
import type { User, Task, File } from '@wasp/entities';
import type { StripePaymentResult } from './types';
import {
GenerateGptResponse,
StripePayment,
UpdateCurrentUser,
UpdateUserById,
CreateTask,
DeleteTask,
UpdateTask,
CreateFile,
} from '@wasp/actions/types';
import { fetchStripeCustomer, createStripeCheckoutSession } from './payments/stripeUtils.js';
import { TierIds } from '@wasp/shared/constants.js';
import { TierIds } from '../shared/constants.js';
import { getUploadFileSignedURLFromS3 } from './file-upload/s3Utils.js';
export const stripePayment: StripePayment<string, StripePaymentResult> = async (tier, context) => {
if (!context.user || !context.user.email) {
if (!context.user) {
throw new HttpError(401);
}
const userEmail = context.user.email;
if (!userEmail) {
throw new HttpError(
403,
'User needs an email to make a payment. If using the usernameAndPassword Auth method, switch to an Auth method that provides an email.'
);
}
let priceId;
if (tier === TierIds.HOBBY) {
@ -34,7 +41,7 @@ export const stripePayment: StripePayment<string, StripePaymentResult> = async (
let customer: Stripe.Customer;
let session: Stripe.Checkout.Session;
try {
customer = await fetchStripeCustomer(context.user.email);
customer = await fetchStripeCustomer(userEmail);
session = await createStripeCheckoutSession({
priceId,
customerId: customer.id,

View File

@ -1,4 +1,4 @@
// import { GetVerificationEmailContentFn, GetPasswordResetEmailContentFn } from '@wasp/types';
// import { type GetVerificationEmailContentFn, type GetPasswordResetEmailContentFn } from "wasp/server/auth";
// export const getVerificationEmailContent: GetVerificationEmailContentFn = ({ verificationLink }) => ({
// subject: 'Verify your email',

View File

@ -1,19 +0,0 @@
// More info on auth config: https://wasp-lang.dev/docs/language/features#social-login-providers-oauth-20
export async function getUserFields(_context: unknown, args: any) {
const email = args.profile.emails[0].value
const username = args.profile.displayName
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || []
const isAdmin = adminEmails.includes(email)
return { email, username, isAdmin };
}
export function config() {
const clientID = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
return {
clientID, // look up from env or elsewhere,
clientSecret, // look up from env or elsewhere,
scope: ['profile', 'email'], // must include at least 'profile' for Google
};
}

View File

@ -1,11 +0,0 @@
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
export default defineAdditionalSignupFields({
isAdmin: (data) => {
if (!data.email) {
return false;
}
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
return adminEmails.includes(data.email as string);
},
});

View File

@ -0,0 +1,45 @@
import { defineUserSignupFields } from 'wasp/auth/providers/types';
export const getUsernameAndPasswordUserFields = defineUserSignupFields({
username: (data: any) => data.username,
});
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
export const getEmailUserFields = defineUserSignupFields({
username: (data: any) => data.email,
isAdmin : (data: any) => adminEmails.includes(data.email),
});
export const getGitHubUserFields = defineUserSignupFields({
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
// instead of ["user"] and access args.profile.username instead
email: (data: any) => data.profile.emails[0].value,
username: (data: any) => data.profile.username,
isAdmin: (data: any) => adminEmails.includes(data.profile.emails[0].value),
});
export function getGitHubAuthConfig() {
return {
clientID: process.env.GITHUB_CLIENT_ID, // look up from env or elsewhere
clientSecret: process.env.GITHUB_CLIENT_SECRET, // look up from env or elsewhere
scope: ['user'],
};
}
export const getGoogleUserFields = defineUserSignupFields({
email: (data: any) => data.profile.emails[0].value,
username: (data: any) => data.profile.displayName,
isAdmin: (data: any) => adminEmails.includes(data.profile.emails[0].value),
});
export function getGoogleAuthConfig() {
const clientID = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
return {
clientID, // look up from env or elsewhere,
clientSecret, // look up from env or elsewhere,
scope: ['profile', 'email'], // must include at least 'profile' for Google
};
}

View File

@ -1,13 +1,13 @@
import HttpError from '@wasp/core/HttpError.js';
import type { DailyStats, GptResponse, User, PageViewSource, Task, File } from '@wasp/entities';
import type {
GetGptResponses,
GetDailyStats,
GetPaginatedUsers,
GetAllTasksByUser,
GetAllFilesByUser,
GetDownloadFileSignedURL,
} from '@wasp/queries/types';
import { type DailyStats, type GptResponse, type User, type PageViewSource, type Task, type File } from 'wasp/entities';
import { HttpError } from 'wasp/server';
import {
type GetGptResponses,
type GetDailyStats,
type GetPaginatedUsers,
type GetAllTasksByUser,
type GetAllFilesByUser,
type GetDownloadFileSignedURL,
} from 'wasp/server/operations';
import { getDownloadFileSignedURLFromS3 } from './file-upload/s3Utils.js';
type DailyStatsWithSources = DailyStats & {
@ -64,7 +64,10 @@ export const getAllFilesByUser: GetAllFilesByUser<void, File[]> = async (_args,
});
};
export const getDownloadFileSignedURL: GetDownloadFileSignedURL<{ key: string }, string> = async ({ key }, _context) => {
export const getDownloadFileSignedURL: GetDownloadFileSignedURL<{ key: string }, string> = async (
{ key },
_context
) => {
return getDownloadFileSignedURLFromS3({ key });
};

View File

@ -1,9 +1,9 @@
import { type User } from 'wasp/entities';
import { faker } from '@faker-js/faker';
import type { PrismaClient } from '@prisma/client';
import type { User } from '@wasp/entities';
import { TierIds } from '@wasp/shared/constants.js';
import { TierIds } from '../../shared/constants.js';
// in a terminal window run `wasp db seed` to seed your dev database with this data
// in a terminal window run `wasp db seed` to seed your dev database with mock user data
export function createRandomUser() {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
@ -16,22 +16,15 @@ export function createRandomUser() {
firstName,
lastName,
}),
password: faker.internet.password({
length: 12,
prefix: 'Aa1!',
}),
createdAt: faker.date.between({ from: new Date('2023-01-01'), to: new Date() }),
lastActiveTimestamp: faker.date.recent(),
isAdmin: false,
isEmailVerified: faker.helpers.arrayElement([true, false]),
stripeId: `cus_${faker.string.uuid()}`,
hasPaid: faker.helpers.arrayElement([true, false]),
sendEmail: false,
subscriptionStatus: faker.helpers.arrayElement(['active', 'canceled', 'past_due', 'deleted']),
datePaid: faker.date.recent(),
credits: faker.number.int({ min: 0, max: 3 }),
emailVerificationSentAt: null,
passwordResetSentAt: null,
checkoutSessionId: null,
subscriptionTier: faker.helpers.arrayElement([TierIds.HOBBY, TierIds.PRO]),
};

View File

@ -1,48 +0,0 @@
// =============================== 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"
],
}

View File

@ -1,5 +1,5 @@
import { User } from '@wasp/entities'
import { Prisma } from '@prisma/client'
import { type User } from 'wasp/entities';
import { Prisma } from '@prisma/client';
export type Context = {
user: User;
@ -30,6 +30,6 @@ export type OpenAIResponse = {
content: string;
};
finish_reason: string;
}
},
];
};
};

View File

@ -1,8 +1,8 @@
import { emailSender } from 'wasp/server/email';
import { type MiddlewareConfigFn } from 'wasp/server';
import { type StripeWebhook } from 'wasp/server/api';
import express from 'express';
import { StripeWebhook } from '@wasp/apis/types';
import type { MiddlewareConfigFn } from '@wasp/middleware';
import { emailSender } from '@wasp/email/index.js';
import { TierIds } from '@wasp/shared/constants.js';
import { TierIds } from '../../shared/constants.js';
import Stripe from 'stripe';
@ -63,7 +63,8 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
}
/**
* and here is an example of handling a different type of product
* and here is an example of handling a product that is not a subscription
* in this case, we are adding 10 credits to the user's account
* make sure to configure it in the Stripe dashboard first!
*/

View File

@ -1,5 +1,5 @@
import { type DailyStatsJob } from 'wasp/server/jobs';
import Stripe from 'stripe';
import type { DailyStatsJob } from '@wasp/jobs/dailyStatsJob.js';
import { getDailyPageViews, getSources } from './plausibleAnalyticsUtils.js';
// import { getDailyPageViews, getSources } from './googleAnalyticsUtils.js';
@ -106,7 +106,7 @@ export const calculateDailyStats: DailyStatsJob<never, void> = async (_args, con
});
}
console.table({ dailyStats })
console.table({ dailyStats });
} catch (error: any) {
console.error('Error calculating daily stats: ', error);
await context.entities.Logs.create({

View File

@ -1,13 +1,13 @@
import { emailSender } from '@wasp/email/index.js'
import { type EmailChecker } from 'wasp/server/jobs';
import type { Email } from '@wasp/email/core/types';
import type { User } from '@wasp/entities'
import type { EmailChecker } from '@wasp/jobs/emailChecker'
import { type User } from 'wasp/entities';
import { emailSender } from 'wasp/server/email';
import { type Email } from 'wasp/server/email/core/types'; // TODO fix after it gets fixed in wasp :)
const emailToSend: Email = {
to: '',
subject: 'The SaaS App Newsletter',
text: "Hey There! \n\nThis is just a newsletter that sends automatically via cron jobs",
text: 'Hey There! \n\nThis is just a newsletter that sends automatically via cron jobs',
html: `<html lang="en">
<head>
<meta charset="UTF-8">
@ -22,20 +22,19 @@ 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 checkAndQueueEmails: EmailChecker<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);
const users = await context.entities.User.findMany({
const users = (await context.entities.User.findMany({
where: {
datePaid: {
equals: twoWeeksFromNow,
},
sendEmail: true,
},
}) as User[];
})) as User[];
if (users.length === 0) {
return;
@ -52,4 +51,4 @@ export const checkAndQueueEmails: EmailChecker<never, void> = async (_args , con
}
})
);
}
};

View File

@ -94,7 +94,6 @@ async function getPrevDayViewsChangePercent() {
startDate: '2daysAgo',
endDate: 'yesterday',
},
],
orderBys: [
{

View File

@ -13,9 +13,7 @@ const isDevEnv = process.env.NODE_ENV !== 'production';
const customerPortalTestUrl = '<your-url-here>'; // TODO: find your test url at https://dashboard.stripe.com/test/settings/billing/portal
const customerPortalProdUrl = '<your-url-here>'; // TODO: add before deploying to production
export const STRIPE_CUSTOMER_PORTAL_LINK = isDevEnv
? customerPortalTestUrl
: customerPortalProdUrl;
export const STRIPE_CUSTOMER_PORTAL_LINK = isDevEnv ? customerPortalTestUrl : customerPortalProdUrl;
checkStripePortalLinksExist({ customerPortalTestUrl, customerPortalProdUrl });
@ -35,16 +33,13 @@ function checkStripePortalLinksExist(links: StripePortalUrls) {
if (testResult.success && prodResult.success) {
consoleMsg.color = '\x1b[32m%s\x1b[0m';
consoleMsg.msg =
'✅ Both STRIPE_CUSTOMER_PORTAL_LINK links defined';
consoleMsg.msg = '✅ Both STRIPE_CUSTOMER_PORTAL_LINK links defined';
} else if (!testResult.success && !prodResult.success) {
consoleMsg.msg =
'⛔️ STRIPE_CUSTOMER_PORTAL_LINK is not defined';
consoleMsg.msg = '⛔️ STRIPE_CUSTOMER_PORTAL_LINK is not defined';
} else if (!testResult.success) {
consoleMsg.msg = '⛔️ STRIPE_CUSTOMER_PORTAL_LINK is not defined for test env';
} else {
consoleMsg.msg =
'⛔️ STRIPE_CUSTOMER_PORTAL_LINK is not defined for prod env';
consoleMsg.msg = '⛔️ STRIPE_CUSTOMER_PORTAL_LINK is not defined for prod env';
}
console.log(consoleMsg.color, consoleMsg.msg);
}

View File

@ -1,28 +0,0 @@
{
"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"]
}
}

1
app/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -1,8 +1,10 @@
const defaultTheme = require('tailwindcss/defaultTheme');
const { resolveProjectPath } = require('wasp/client');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// content: ['./src/**/*.{js,jsx,ts,tsx}'],
content: [resolveProjectPath('./src/**/*.{js,jsx,ts,tsx}')],
darkMode: 'class',
theme: {
extend: {

36
app/tsconfig.json Normal file
View File

@ -0,0 +1,36 @@
// =============================== 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": {
"forceConsistentCasingInFileNames": false,
// JSX support
"jsx": "preserve",
"strict": true,
// Allow default imports.
"esModuleInterop": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"types": [
// This is needed to properly support Vitest testing with jest-dom matchers.
// Types for jest-dom are not recognized automatically and Typescript complains
// about missing types e.g. when using `toBeInTheDocument` and other matchers.
"@testing-library/jest-dom"
],
// 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"
],
}

7
app/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
export default defineConfig({
server: {
open: true,
},
})

View File

@ -18,12 +18,30 @@ export default defineConfig({
},
}),
starlight({
title: 'Your SaaS',
title: 'Your SaaS',
description: 'Documentation for your SaaS.',
logo: {
src: '/src/assets/logo.png',
alt: 'Your SaaS',
},
head: [ // Add your script tags here. Below is an example for Google analytics, etc.
{
tag: 'script',
attrs: {
src: 'https://www.googletagmanager.com/gtag/js?id=<YOUR-GOOGLE-ANALYTICS-ID>',
},
},
{
tag: 'script',
content: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<YOUR-GOOGLE-ANALYTICS-ID>');
`,
},
],
editLink: {
baseUrl: 'https://github.com/<your-repo>',
},
@ -41,16 +59,11 @@ export default defineConfig({
sidebar: [
{
label: 'Start Here',
items: [
{ label: 'Introduction', link: '/' },
],
items: [{ label: 'Introduction', link: '/' }],
},
{
label: 'Guides',
items: [
{ label: 'Example Guide', link: '/guides/example/' },
],
items: [{ label: 'Example Guide', link: '/guides/example/' }],
},
],
}),