mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-04-11 21:39:03 +02:00
Merge branch 'main-v0.12'
This commit is contained in:
commit
c7ca392987
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.wasp linguist-language=TypeScript
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,5 +2,6 @@
|
||||
*/.env.server
|
||||
*/.env.client
|
||||
*/.DS_Store
|
||||
.DS_Store
|
||||
*/migrations
|
||||
*/node_modules
|
||||
*/migrations
|
||||
.DS_Store
|
151
app/main.wasp
151
app/main.wasp
@ -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 setAdditionalUserFields from "@server/auth/setAdditionalUserFields.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.28.0"),
|
||||
("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:
|
||||
@ -96,15 +80,11 @@ app SaaSTemplate {
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String?
|
||||
username String @unique
|
||||
password String
|
||||
email String? @unique
|
||||
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
|
||||
|
10568
app/package-lock.json
generated
Normal file
10568
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
app/package.json
Normal file
32
app/package.json
Normal 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
0
app/public/.gitkeep
Normal file
BIN
app/public/banner.png
Normal file
BIN
app/public/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 840 KiB |
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { DailyStats } from "@wasp/entities";
|
||||
import { type DailyStats } from 'wasp/entities';
|
||||
|
||||
export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?:DailyStats[], isLoading?: boolean }
|
||||
export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?: DailyStats[]; isLoading?: boolean };
|
||||
|
@ -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
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { type AuthUser } from 'wasp/auth/types';
|
||||
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;
|
||||
setSidebarOpen: (arg0: boolean) => void;
|
||||
user?: Partial<User>;
|
||||
user?: AuthUser | null;
|
||||
}) => {
|
||||
return (
|
||||
<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 --> */}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
@ -12,24 +12,22 @@ const DefaultLayout: FC<Props> = ({ children }) => {
|
||||
const { data: user } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="dark:bg-boxdark-2 dark:text-bodydark">
|
||||
<div className='dark:bg-boxdark-2 dark:text-bodydark'>
|
||||
{/* <!-- ===== Page Wrapper Start ===== --> */}
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<div className='flex h-screen overflow-hidden'>
|
||||
{/* <!-- ===== Sidebar Start ===== --> */}
|
||||
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||
{/* <!-- ===== Sidebar End ===== --> */}
|
||||
|
||||
{/* <!-- ===== Content Area Start ===== --> */}
|
||||
<div className="relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
|
||||
<div className='relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden'>
|
||||
{/* <!-- ===== Header Start ===== --> */}
|
||||
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} user={user} />
|
||||
{/* <!-- ===== Header End ===== --> */}
|
||||
|
||||
{/* <!-- ===== Main Content Start ===== --> */}
|
||||
<main>
|
||||
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||
{children}
|
||||
</div>
|
||||
<div className='mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10'>{children}</div>
|
||||
</main>
|
||||
{/* <!-- ===== Main Content End ===== --> */}
|
||||
</div>
|
||||
|
@ -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();
|
||||
|
@ -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 (
|
||||
|
@ -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';
|
||||
import { type GeneratedSchedule } from '../../shared/types';
|
||||
|
@ -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>('');
|
||||
|
@ -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'>
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
// import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword';
|
||||
// import { ForgotPasswordForm } from 'wasp/client/auth';
|
||||
// import { AuthWrapper } from './authWrapper';
|
||||
|
||||
// export function RequestPasswordReset() {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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 |
@ -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"
|
||||
],
|
||||
}
|
1
app/src/client/vite-env.d.ts
vendored
1
app/src/client/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="../../.wasp/out/web-app/node_modules/vite/client" />
|
@ -1,28 +1,36 @@
|
||||
import Stripe from 'stripe';
|
||||
import HttpError from '@wasp/core/HttpError.js';
|
||||
import type { User, Task, File } from '@wasp/entities';
|
||||
import type { StripePaymentResult, GeneratedSchedule } from '../shared/types';
|
||||
import { type User, type Task, type File } from 'wasp/entities';
|
||||
import { HttpError } from 'wasp/server';
|
||||
import {
|
||||
GenerateGptResponse,
|
||||
StripePayment,
|
||||
UpdateCurrentUser,
|
||||
UpdateUserById,
|
||||
CreateTask,
|
||||
DeleteTask,
|
||||
UpdateTask,
|
||||
CreateFile,
|
||||
} from '@wasp/actions/types';
|
||||
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 type { StripePaymentResult } from './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';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
|
||||
|
||||
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) {
|
||||
@ -36,7 +44,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,
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
|
||||
|
||||
export default defineAdditionalSignupFields({
|
||||
// username: (data) => {
|
||||
// return data.email as string
|
||||
// },
|
||||
isAdmin: (data) => {
|
||||
if (!data.email) {
|
||||
return false;
|
||||
}
|
||||
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
|
||||
return adminEmails.includes(data.email as string);
|
||||
},
|
||||
});
|
45
app/src/server/auth/setUsername.ts
Normal file
45
app/src/server/auth/setUsername.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
@ -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 });
|
||||
};
|
||||
|
||||
|
@ -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]),
|
||||
};
|
||||
|
@ -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"
|
||||
],
|
||||
}
|
@ -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!
|
||||
*/
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -94,7 +94,6 @@ async function getPrevDayViewsChangePercent() {
|
||||
startDate: '2daysAgo',
|
||||
endDate: 'yesterday',
|
||||
},
|
||||
|
||||
],
|
||||
orderBys: [
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
1
app/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
@ -1,8 +1,10 @@
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
const { resolveProjectPath } = require('wasp/dev');
|
||||
|
||||
/** @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
36
app/tsconfig.json
Normal 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": {
|
||||
// JSX support
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
// Allow default imports.
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"typeRoots": [
|
||||
// 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.
|
||||
"node_modules/@testing-library",
|
||||
// Specifying type roots overrides the default behavior of looking at the
|
||||
// node_modules/@types folder so we had to list it explicitly.
|
||||
// Source 1: https://www.typescriptlang.org/tsconfig#typeRoots
|
||||
// Source 2: https://github.com/testing-library/jest-dom/issues/546#issuecomment-1889884843
|
||||
"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": ".wasp/phantom"
|
||||
}
|
||||
}
|
7
app/vite.config.ts
Normal file
7
app/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
})
|
@ -7,53 +7,83 @@ import tailwind from "@astrojs/tailwind";
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://opensaas.sh',
|
||||
integrations: [starlightBlog({
|
||||
title: 'Blog',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
title: 'Dev Rel @ Wasp',
|
||||
picture: '/CRAIG_ROCK.png',
|
||||
// Images in the `public` directory are supported.
|
||||
url: 'https://wasp-lang.dev'
|
||||
}
|
||||
}
|
||||
}), starlight({
|
||||
title: 'Your SaaS',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
description: 'Documentation for your SaaS.',
|
||||
logo: {
|
||||
src: '/src/assets/logo.png',
|
||||
alt: 'Your SaaS'
|
||||
},
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/<your-repo>'
|
||||
},
|
||||
components: {
|
||||
SiteTitle: './src/components/MyHeader.astro',
|
||||
MarkdownContent: 'starlight-blog/overrides/MarkdownContent.astro',
|
||||
Sidebar: 'starlight-blog/overrides/Sidebar.astro'
|
||||
// ThemeSelect: 'starlight-blog/overrides/ThemeSelect.astro',
|
||||
},
|
||||
integrations: [
|
||||
starlightBlog({
|
||||
title: 'Blog',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
title: 'Dev Rel @ Wasp',
|
||||
picture: '/CRAIG_ROCK.png',
|
||||
// Images in the `public` directory are supported.
|
||||
url: 'https://wasp-lang.dev',
|
||||
},
|
||||
},
|
||||
}),
|
||||
starlight({
|
||||
title: 'Your SaaS',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
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>',
|
||||
},
|
||||
components: {
|
||||
SiteTitle: './src/components/MyHeader.astro',
|
||||
MarkdownContent: 'starlight-blog/overrides/MarkdownContent.astro',
|
||||
Sidebar: 'starlight-blog/overrides/Sidebar.astro',
|
||||
// ThemeSelect: 'starlight-blog/overrides/ThemeSelect.astro',
|
||||
},
|
||||
|
||||
social: {
|
||||
github: 'https://github.com/wasp-lang/open-saas',
|
||||
twitter: 'https://twitter.com/wasp_lang',
|
||||
discord: 'https://discord.gg/aCamt5wCpS'
|
||||
},
|
||||
sidebar: [{
|
||||
label: 'Start Here',
|
||||
items: [{
|
||||
label: 'Introduction',
|
||||
link: '/'
|
||||
}]
|
||||
}, {
|
||||
label: 'Guides',
|
||||
items: [{
|
||||
label: 'Example Guide',
|
||||
link: '/guides/example/'
|
||||
}]
|
||||
}]
|
||||
}), tailwind({applyBaseStyles: false})]
|
||||
});
|
||||
social: {
|
||||
github: 'https://github.com/wasp-lang/open-saas',
|
||||
twitter: 'https://twitter.com/wasp_lang',
|
||||
discord: 'https://discord.gg/aCamt5wCpS',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Start Here',
|
||||
items: [
|
||||
{
|
||||
label: 'Introduction',
|
||||
link: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{
|
||||
label: 'Example Guide',
|
||||
link: '/guides/example/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
tailwind({ applyBaseStyles: false }),
|
||||
],
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user