mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-05-20 09:10:11 +02:00
fix seed, stats, footer, etc
This commit is contained in:
parent
646e33f881
commit
7850db18a3
@ -11,6 +11,9 @@ PRO_SUBSCRIPTION_PRICE_ID=price_...
|
||||
# after downloading starting the stripe cli (https://stripe.com/docs/stripe-cli) with `stripe listen --forward-to localhost:3001/stripe-webhook` it will output your signing secret
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# set this as a comma-separated list of emails you want to give admin privileges to upon registeration
|
||||
ADMIN_EMAILS=me@example.com,you@example.com,them@example.com
|
||||
|
||||
# this needs to be a string at least 32 characters long
|
||||
JWT_SECRET=
|
||||
|
||||
|
@ -60,7 +60,8 @@ export default defineConfig({
|
||||
{
|
||||
label: 'General',
|
||||
items: [
|
||||
{ label: 'User Permissions', link: 'general/user-permissions/' },
|
||||
{ label: 'Admin Dashboard', link: '/general/admin-dashboard/' },
|
||||
{ label: 'User Permissions', link: '/general/user-permissions/' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
62
docs/src/content/docs/general/admin-dashboard.md
Normal file
62
docs/src/content/docs/general/admin-dashboard.md
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Admin Dashboard
|
||||
---
|
||||
This is a reference on how the Admin dashboard is set up and works.
|
||||
|
||||
## Permissions
|
||||
|
||||
The Admin dashboard is only accessible to users with the `isAdmin` field set to true.
|
||||
|
||||
```tsx title="main.wasp" {5}
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
isAdmin Boolean @default(false)
|
||||
//...
|
||||
```
|
||||
|
||||
To give yourself administrator priveledges, make sure you add your email adderesses to the `ADMIN_EMAILS` environment variable in `.env.server` file before registering/logging in with that email address.
|
||||
|
||||
```sh title=".env.server"
|
||||
ADMIN_EMAILS=me@example.com
|
||||
```
|
||||
|
||||
if you want to give administrator priveledges to other users, you can do so by adding them to `ADMIN_EMAILS` as a comma-separated list.
|
||||
|
||||
```sh title=".env.server"
|
||||
ADMIN_EMAILS=me@example.com,you@example.com,them@example.com
|
||||
```
|
||||
|
||||
## Admin Dashboard Pages
|
||||
|
||||
### Dashboard
|
||||
The Admin dashboard is a single place for you to view your most important metrics and perform some admin tasks. At the moment, it pulls data from:
|
||||
|
||||
<!-- TODO: add photo -->
|
||||
|
||||
- [Stripe](/guides/stripe-integration):
|
||||
- total revenue
|
||||
- revenue for each day of the past week
|
||||
- [Google or Plausible](/guides/analytics):
|
||||
- total number of page views (non-unique)
|
||||
- percentage change in page views from the previous day
|
||||
- top sources/referrers with unique visitor count (i.e. how many people came from that source to your app)
|
||||
- Database:
|
||||
- total number of registered users
|
||||
- daily change in number of registered users
|
||||
- total number of paying users
|
||||
- daily change in number of paying users
|
||||
|
||||
For a guide on how to integrate these services, check out the [Stripe](/guides/stripe-integration) and [Analytics guide](/guides/analytics) of the docs.
|
||||
|
||||
<!-- TODO: add repo links -->
|
||||
:::tip[Help us improve]
|
||||
We're always looking to improve the Admin dashboard. If you feel something is missing or could be improved, consider [opening an issue]() or [submitting a pull request]()
|
||||
:::
|
||||
|
||||
### Users
|
||||
The Users page is where you can view all your users and their most important details. You can also search and filter users by:
|
||||
- email address
|
||||
- subscription status
|
||||
|
@ -9,7 +9,15 @@ Plausible is an open-source, privacy-friendly alternative to Google Analytics. I
|
||||
|
||||
## Plausible
|
||||
<!-- TODO add plausible -->
|
||||
*coming soon... until then, check out the [official documentation](https://plausible.io/docs)*
|
||||
|
||||
*coming soon...*
|
||||
*until then, check out the [official documentation](https://plausible.io/docs)*
|
||||
|
||||
:::tip[Contribute!]
|
||||
If you'd like to help us write this guide, click the "Edit page" button at the bottom of this page
|
||||
|
||||
As a completely free, open-source project, we appreciate any help 🙏
|
||||
:::
|
||||
|
||||
## Google Analytics
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Email Sending
|
||||
---
|
||||
|
||||
This reference explains when emails
|
81
docs/src/content/docs/guides/email-sending.mdx
Normal file
81
docs/src/content/docs/guides/email-sending.mdx
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
title: Email Sending
|
||||
---
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
This guide explains how to use the integrated email sender and how you can integrate your own account in this template.
|
||||
|
||||
## Sending Emails
|
||||
|
||||
In the `main.wasp` config file, you'll see we've got the email sender set up for you:
|
||||
|
||||
```tsx title="main.wasp"
|
||||
app SaaSTemplate {
|
||||
// ...
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
defaultFrom: {
|
||||
name: "Open SaaS App",
|
||||
// make sure this address is the same you registered your SendGrid or MailGun account with!
|
||||
email: "my@email.com"
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
This means that you can send emails from your app using the `send` function from the `email` module:
|
||||
|
||||
```tsx title="src/server/webhooks.ts"
|
||||
import { emailSender } from "@wasp/email/index.js";
|
||||
|
||||
//...
|
||||
|
||||
if (subscription.cancel_at_period_end) {
|
||||
await emailSender.send({
|
||||
to: customer.email,
|
||||
subject: 'We hate to see you go :(',
|
||||
text: 'We hate to see you go. Here is a sweet offer...',
|
||||
html: 'We hate to see you go. Here is a sweet offer...',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, you can see that we're sending an email to the customer when we receive a cancel subscription event within the Stripe webhook.
|
||||
|
||||
This is a powerful feature and super simple to use.
|
||||
|
||||
:::tip[Sending Emails in Development]
|
||||
In the `.env.server` file, we've set the `SEND_EMAILS_IN_DEVELOPMENT` env variable to true. This means that emails will be sent in development mode.
|
||||
|
||||
This is useful for testing, but you can turn it off by setting it to false, and the emails will be logged to the console instead.
|
||||
:::
|
||||
|
||||
## Integrate your email sender
|
||||
|
||||
To set up your email sender, you first need an account with one of the supported email providers.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="SendGrid">
|
||||
- Register at SendGrid.com and then get your [API KEYS](https://app.sendgrid.com/settings/api_keys).
|
||||
- Copy yours to the `.env.server` file under the `SENDGRID_API_KEY` variable.
|
||||
</TabItem>
|
||||
<TabItem label="MailGun">
|
||||
- Go to [Mailgun](https://mailgun.com) and create an account.
|
||||
- Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key.
|
||||
- Copy the API key and add it to your .env.server file under the `MAILGUN_API_KEY=` variable.
|
||||
- Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain.
|
||||
- Copy the domain and add it to your .env.server file as `MAILGUN_DOMAIN=`.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Make sure to change the `defaultFrom` email address in the `main.wasp` file to the same email address you used to register your account with.
|
||||
|
||||
```tsx title="main.wasp" {5}
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
defaultFrom: {
|
||||
name: "Open SaaS App",
|
||||
email: "my@email.com"
|
||||
},
|
||||
```
|
||||
|
||||
If you want more detailed info, or would like to use SMTP, check out the [Wasp docs](https://wasp-lang.dev/docs/advanced/email).
|
22
main.wasp
22
main.wasp
@ -2,7 +2,7 @@ app SaaSTemplate {
|
||||
wasp: {
|
||||
version: "^0.11.6"
|
||||
},
|
||||
title: "My SaaS App",
|
||||
title: "My Open SaaS App",
|
||||
head: [
|
||||
"<meta property='og:type' content='website' />",
|
||||
"<meta property='og:url' content='https://mySaaSapp.com' />", // TODO change url
|
||||
@ -26,7 +26,7 @@ app SaaSTemplate {
|
||||
methods: {
|
||||
email: {
|
||||
fromField: {
|
||||
name: "SaaS App",
|
||||
name: "Open SaaS App",
|
||||
// make sure this address is the same you registered your SendGrid or MailGun account with!
|
||||
email: "vince@wasp-lang.dev"
|
||||
},
|
||||
@ -44,13 +44,15 @@ app SaaSTemplate {
|
||||
configFn: import { config } from "@server/auth/google.js",
|
||||
},
|
||||
},
|
||||
signup: {
|
||||
additionalFields: import setAdminUsers from "@server/auth/setAdminUsers.js",
|
||||
},
|
||||
onAuthFailedRedirectTo: "/",
|
||||
},
|
||||
db: {
|
||||
system: PostgreSQL,
|
||||
seeds: [
|
||||
import { devSeedUsers } from "@server/scripts/usersSeed.js",
|
||||
|
||||
]
|
||||
},
|
||||
client: {
|
||||
@ -64,6 +66,7 @@ app SaaSTemplate {
|
||||
email: "vince@wasp-lang.dev" // TODO change to generic email before pushing to github
|
||||
},
|
||||
},
|
||||
// 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"),
|
||||
@ -104,7 +107,7 @@ entity User {=psl
|
||||
sendEmail Boolean @default(false)
|
||||
datePaid DateTime?
|
||||
credits Int @default(3)
|
||||
relatedObject RelatedObject[]
|
||||
gptResponses GptResponse[]
|
||||
externalAuthAssociations SocialLogin[]
|
||||
contactFormMessages ContactFormMessage[]
|
||||
psl=}
|
||||
@ -120,7 +123,7 @@ entity SocialLogin {=psl
|
||||
psl=}
|
||||
|
||||
// This can be anything. In most cases, this will be your product
|
||||
entity RelatedObject {=psl
|
||||
entity GptResponse {=psl
|
||||
id String @id @default(uuid())
|
||||
content String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
@ -205,6 +208,7 @@ page EmailVerificationPage {
|
||||
|
||||
route GptRoute { path: "/gpt", to: GptPage }
|
||||
page GptPage {
|
||||
authRequired: true,
|
||||
component: import GptPage from "@client/app/GptPage"
|
||||
}
|
||||
|
||||
@ -287,7 +291,7 @@ page AdminUIButtonsPage {
|
||||
|
||||
action generateGptResponse {
|
||||
fn: import { generateGptResponse } from "@server/actions.js",
|
||||
entities: [User, RelatedObject]
|
||||
entities: [User, GptResponse]
|
||||
}
|
||||
|
||||
action stripePayment {
|
||||
@ -307,9 +311,9 @@ action updateUserById {
|
||||
|
||||
// 📚 Queries
|
||||
|
||||
query getRelatedObjects {
|
||||
fn: import { getRelatedObjects } from "@server/queries.js",
|
||||
entities: [User, RelatedObject]
|
||||
query getGptResponses {
|
||||
fn: import { getGptResponses } from "@server/queries.js",
|
||||
entities: [User, GptResponse]
|
||||
}
|
||||
|
||||
query getDailyStats {
|
||||
|
25
migrations/20231130143812_gpt_response/migration.sql
Normal file
25
migrations/20231130143812_gpt_response/migration.sql
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `RelatedObject` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "RelatedObject" DROP CONSTRAINT "RelatedObject_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "RelatedObject";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GptResponse" (
|
||||
"id" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "GptResponse_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GptResponse" ADD CONSTRAINT "GptResponse_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -1,6 +1,6 @@
|
||||
import { User } from '@wasp/entities';
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getRelatedObjects from '@wasp/queries/getRelatedObjects'
|
||||
import getGptResponses from '@wasp/queries/getGptResponses'
|
||||
import logout from '@wasp/auth/logout';
|
||||
import { useState, Dispatch, SetStateAction } from 'react';
|
||||
import { Link } from '@wasp/router'
|
||||
@ -10,7 +10,7 @@ import { TierIds } from '@wasp/shared/const';
|
||||
export default function AccountPage({ user }: { user: User }) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const { data: relatedObjects, isLoading: isLoadingRelatedObjects } = useQuery(getRelatedObjects);
|
||||
const { data: gptResponses, isLoading: isLoadingGptResponses } = useQuery(getGptResponses);
|
||||
|
||||
return (
|
||||
<div className='mt-10 px-6'>
|
||||
@ -45,10 +45,10 @@ export default function AccountPage({ user }: { user: User }) {
|
||||
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>I'm a cool customer.</dd>
|
||||
</div>
|
||||
<div className='py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6'>
|
||||
<dt className='text-sm font-medium text-gray-500'>Most Recent User RelatedObject</dt>
|
||||
<dt className='text-sm font-medium text-gray-500'>Most Recent GPT Response</dt>
|
||||
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
|
||||
{!!relatedObjects && relatedObjects.length > 0
|
||||
? relatedObjects[relatedObjects.length - 1].content
|
||||
{!!gptResponses && gptResponses.length > 0
|
||||
? gptResponses[gptResponses.length - 1].content
|
||||
: "You don't have any at this time."}
|
||||
</dd>
|
||||
</div>
|
||||
|
@ -7,9 +7,9 @@ import DropdownUser from './DropdownUser';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'GPT Wrapper', href: '/gpt' },
|
||||
{ name: 'Documentation', href: 'https://saas-template.gitbook.io/test' },
|
||||
{ name: 'Blog', href: 'https://saas-template.gitbook.io/posts/' },
|
||||
];
|
||||
{ name: 'Documentation', href: '#' }, // TODO: add link to docs
|
||||
{ name: 'Blog', href: '#' }, // TODO: add link to blog
|
||||
];
|
||||
|
||||
export default function AppNavBar() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
@ -395,7 +395,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQs */}
|
||||
{/* FAQ */}
|
||||
<div className='mx-auto max-w-2xl divide-y divide-gray-900/10 px-6 pb-8 sm:pb-24 sm:pt-12 lg:max-w-7xl lg:px-8 lg:pb-32'>
|
||||
<h2 className='text-2xl font-bold leading-10 tracking-tight text-gray-900'>Frequently asked questions</h2>
|
||||
<dl className='mt-10 space-y-8 divide-y divide-gray-900/10'>
|
||||
@ -409,123 +409,38 @@ export default function LandingPage() {
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* CTA section */}
|
||||
<div className='relative -z-10 mt-32 px-6 lg:px-8'>
|
||||
<div
|
||||
className='absolute inset-x-0 top-1/2 -z-10 flex -translate-y-1/2 transform-gpu justify-center overflow-hidden blur-3xl sm:bottom-0 sm:right-[calc(50%-6rem)] sm:top-auto sm:translate-y-0 sm:transform-gpu sm:justify-end'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<div
|
||||
className='aspect-[1108/632] w-[69.25rem] flex-none bg-gradient-to-r from-[#ff80b5] to-[#9089fc] opacity-25'
|
||||
style={{
|
||||
clipPath:
|
||||
'polygon(73.6% 48.6%, 91.7% 88.5%, 100% 53.9%, 97.4% 18.1%, 92.5% 15.4%, 75.7% 36.3%, 55.3% 52.8%, 46.5% 50.9%, 45% 37.4%, 50.3% 13.1%, 21.3% 36.2%, 0.1% 0.1%, 5.4% 49.1%, 21.4% 36.4%, 58.9% 100%, 73.6% 48.6%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-auto max-w-2xl text-center'>
|
||||
<h2 className='text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl'>
|
||||
Boost your productivity.
|
||||
<br />
|
||||
Start using our app today.
|
||||
</h2>
|
||||
<p className='mx-auto mt-6 max-w-xl text-lg leading-8 text-gray-600'>
|
||||
Incididunt sint fugiat pariatur cupidatat consectetur sit cillum anim id veniam aliqua proident excepteur
|
||||
commodo do ea.
|
||||
</p>
|
||||
<div className='mt-10 flex items-center justify-center gap-x-6'>
|
||||
<a
|
||||
href='https://github.com/wasp-lang/saas-template-gpt'
|
||||
className='rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='absolute left-1/2 right-0 top-full -z-10 hidden -translate-y-1/2 transform-gpu overflow-hidden blur-3xl sm:block'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<div
|
||||
className='aspect-[1155/678] w-[72.1875rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-30'
|
||||
style={{
|
||||
clipPath:
|
||||
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='mx-auto mt-32 max-w-7xl px-6 lg:px-8'>
|
||||
<footer
|
||||
aria-labelledby='footer-heading'
|
||||
className='relative border-t border-gray-900/10 py-24 sm:mt-56 sm:py-32'
|
||||
>
|
||||
<div className='mx-auto mt-6 max-w-7xl px-6 lg:px-8'>
|
||||
<footer aria-labelledby='footer-heading' className='relative border-t border-gray-900/10 py-24 sm:mt-32 '>
|
||||
<h2 id='footer-heading' className='sr-only'>
|
||||
Footer
|
||||
</h2>
|
||||
<div className='xl:grid xl:grid-cols-3 xl:gap-8'>
|
||||
<img
|
||||
className='h-7'
|
||||
src='https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600'
|
||||
alt='Company name'
|
||||
/>
|
||||
<div className='mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0'>
|
||||
<div className='md:grid md:grid-cols-2 md:gap-8'>
|
||||
<div>
|
||||
<h3 className='text-sm font-semibold leading-6 text-gray-900'>Solutions</h3>
|
||||
<ul role='list' className='mt-6 space-y-4'>
|
||||
{footerNavigation.solutions.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a href={item.href} className='text-sm leading-6 text-gray-600 hover:text-gray-900'>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='mt-10 md:mt-0'>
|
||||
<h3 className='text-sm font-semibold leading-6 text-gray-900'>Support</h3>
|
||||
<ul role='list' className='mt-6 space-y-4'>
|
||||
{footerNavigation.support.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a href={item.href} className='text-sm leading-6 text-gray-600 hover:text-gray-900'>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className='md:grid md:grid-cols-2 md:gap-8'>
|
||||
<div>
|
||||
<h3 className='text-sm font-semibold leading-6 text-gray-900'>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'>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='mt-10 md:mt-0'>
|
||||
<h3 className='text-sm font-semibold leading-6 text-gray-900'>Legal</h3>
|
||||
<ul role='list' className='mt-6 space-y-4'>
|
||||
{footerNavigation.legal.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a href={item.href} className='text-sm leading-6 text-gray-600 hover:text-gray-900'>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-end mt-10 gap-20'>
|
||||
<div>
|
||||
<h3 className='text-sm font-semibold leading-6 text-gray-900'>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'>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className='text-sm font-semibold leading-6 text-gray-900'>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'>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -25,12 +25,24 @@ export const features = [
|
||||
"No SaaS is complete without payments. That's why subscriptions and there necessary webhooks are built-in!",
|
||||
icon: '💸',
|
||||
},
|
||||
{
|
||||
name: 'Admin Dashboard',
|
||||
description:
|
||||
"Graphs! Tables! Analytics all in one place! Ooooooooooh! Ahhhhhhhhh!",
|
||||
icon: '📈',
|
||||
},
|
||||
{
|
||||
name: 'Email Sending',
|
||||
description:
|
||||
"Email sending is built-in and pre-configured. Combine it with Wasp's cron jobs feature to easily send emails to your customers.",
|
||||
icon: '📧',
|
||||
},
|
||||
{
|
||||
name: 'OpenAI Integration',
|
||||
description:
|
||||
"Technology is changing rapidly. Ship your new AI-powered app before it's already obsolete!",
|
||||
icon: '🤖',
|
||||
},
|
||||
{
|
||||
name: 'Deploy Anywhere',
|
||||
description: 'You own all your code, so deploy it wherever you want!',
|
||||
@ -69,35 +81,26 @@ export const tiers = [
|
||||
export const faqs = [
|
||||
{
|
||||
id: 1,
|
||||
question: "What's the best thing about Switzerland?",
|
||||
question: "Why is this amazing SaaS Template free and open-source?",
|
||||
answer:
|
||||
"I don't know, but the flag is a big plus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.",
|
||||
"Because open-source is cool, and we love you ❤️",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: "What's Wasp?",
|
||||
answer:
|
||||
"It's the fastest way to develop full-stack React + NodeJS + Prisma apps. It's what gives this template superpowers.",
|
||||
},
|
||||
// More questions...
|
||||
];
|
||||
export const footerNavigation = {
|
||||
solutions: [
|
||||
{ name: 'Hosting', href: '#' },
|
||||
{ name: 'Data Services', href: '#' },
|
||||
{ name: 'Uptime Monitoring', href: '#' },
|
||||
{ name: 'Enterprise Services', href: '#' },
|
||||
],
|
||||
support: [
|
||||
{ name: 'Pricing', href: '#' },
|
||||
{ name: 'Documentation', href: '#' },
|
||||
{ name: 'Guides', href: '#' },
|
||||
{ name: 'API Reference', href: '#' },
|
||||
app: [
|
||||
{ name: 'Pricing', href: '#pricing' },
|
||||
{ name: 'Documentation', href: '#' }, // TODO: fill in
|
||||
{ name: 'Blog', href: '#' },
|
||||
],
|
||||
company: [
|
||||
{ name: 'About', href: '#' },
|
||||
{ name: 'Blog', href: '#' },
|
||||
{ name: 'Jobs', href: '#' },
|
||||
{ name: 'Press', href: '#' },
|
||||
{ name: 'Partners', href: '#' },
|
||||
],
|
||||
legal: [
|
||||
{ name: 'Claim', href: '#' },
|
||||
{ name: 'Privacy', href: '#' },
|
||||
{ name: 'Terms', href: '#' },
|
||||
{ name: 'Terms of Service', href: '#' },
|
||||
],
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Stripe from 'stripe';
|
||||
import fetch from 'node-fetch';
|
||||
import HttpError from '@wasp/core/HttpError.js';
|
||||
import type { RelatedObject, User } from '@wasp/entities';
|
||||
import type { GptResponse, User } from '@wasp/entities';
|
||||
import type { GenerateGptResponse, StripePayment } from '@wasp/actions/types';
|
||||
import type { StripePaymentResult, OpenAIResponse } from './types';
|
||||
import { UpdateCurrentUser, UpdateUserById } from '@wasp/actions/types';
|
||||
@ -56,7 +56,7 @@ type GptPayload = {
|
||||
temperature: number;
|
||||
};
|
||||
|
||||
export const generateGptResponse: GenerateGptResponse<GptPayload, RelatedObject> = async (
|
||||
export const generateGptResponse: GenerateGptResponse<GptPayload, GptResponse> = async (
|
||||
{ instructions, command, temperature },
|
||||
context
|
||||
) => {
|
||||
@ -106,7 +106,7 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, RelatedObject>
|
||||
|
||||
const json = (await response.json()) as OpenAIResponse;
|
||||
console.log('response json', json);
|
||||
return context.entities.RelatedObject.create({
|
||||
return context.entities.GptResponse.create({
|
||||
data: {
|
||||
content: json?.choices[0].message.content,
|
||||
user: { connect: { id: context.user.id } },
|
||||
|
@ -3,7 +3,9 @@
|
||||
export async function getUserFields(_context: unknown, args: any) {
|
||||
console.log('args', args.profile)
|
||||
const email = args.profile.emails[0].value
|
||||
return { email };
|
||||
const adminEmails = process.env.ADMIN_EMAILS?.split(',') || []
|
||||
const isAdmin = adminEmails.includes(email)
|
||||
return { email, isAdmin };
|
||||
}
|
||||
|
||||
export function config() {
|
||||
|
11
src/server/auth/setAdminUsers.ts
Normal file
11
src/server/auth/setAdminUsers.ts
Normal file
@ -0,0 +1,11 @@
|
||||
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);
|
||||
},
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import HttpError from '@wasp/core/HttpError.js';
|
||||
import type { DailyStats, RelatedObject, User, PageViewSource } from '@wasp/entities';
|
||||
import type { DailyStats, GptResponse, User, PageViewSource } from '@wasp/entities';
|
||||
import type {
|
||||
GetRelatedObjects,
|
||||
GetGptResponses,
|
||||
GetDailyStats,
|
||||
GetPaginatedUsers,
|
||||
} from '@wasp/queries/types';
|
||||
@ -15,11 +15,11 @@ type DailyStatsValues = {
|
||||
weeklyStats: DailyStatsWithSources[];
|
||||
};
|
||||
|
||||
export const getRelatedObjects: GetRelatedObjects<void, RelatedObject[]> = async (args, context) => {
|
||||
export const getGptResponses: GetGptResponses<void, GptResponse[]> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
return context.entities.RelatedObject.findMany({
|
||||
return context.entities.GptResponse.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: context.user.id,
|
||||
|
@ -4,10 +4,8 @@ import type { User } from '@wasp/entities';
|
||||
|
||||
// in a terminal window run `wasp db seed` to seed your dev database with this data
|
||||
|
||||
let prevUserId = 0;
|
||||
export function createRandomUser(): Partial<User> {
|
||||
const user: Partial<User> = {
|
||||
id: ++prevUserId,
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password({
|
||||
length: 12,
|
||||
|
@ -192,26 +192,9 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
}
|
||||
};
|
||||
|
||||
// MIDDELWARE EXAMPLE
|
||||
// const defaultGlobalMiddleware: MiddlewareConfig = new Map([
|
||||
// ['helmet', helmet()],
|
||||
// ['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
// ['logger', logger('dev')],
|
||||
// ['express.json', express.json()],
|
||||
// ['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
// ['cookieParser', cookieParser()],
|
||||
// ]);
|
||||
|
||||
// This allows us to override Wasp's defaults and parse the raw body of the request from Stripe to verify the signature
|
||||
export const stripeMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
middlewareConfig.delete('express.json');
|
||||
middlewareConfig.set('express.raw', express.raw({ type: 'application/json' }));
|
||||
return middlewareConfig;
|
||||
|
||||
// let updatedMiddlewareConfig = new Map([
|
||||
// // New entry as an array: [key, value]
|
||||
// ['express.raw', express.raw({ type: 'application/json' })],
|
||||
// ...Array.from(middlewareConfig.entries()),
|
||||
// ]);
|
||||
|
||||
// return updatedMiddlewareConfig;
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { DailyStatsJob } from '@wasp/jobs/dailyStatsJob';
|
||||
import type { DailyStats } from '@wasp/entities';
|
||||
import Stripe from 'stripe';
|
||||
// import { getDailyPageViews, getSources } from './plausibleAnalyticsUtils.js';
|
||||
import { getDailyPageViews, getSources } from './googleAnalyticsUtils.js';
|
||||
|
@ -98,6 +98,15 @@ async function getPrevDayViewsChangePercent() {
|
||||
startDate: '2daysAgo',
|
||||
endDate: 'yesterday',
|
||||
},
|
||||
|
||||
],
|
||||
orderBys: [
|
||||
{
|
||||
dimension: {
|
||||
dimensionName: 'date',
|
||||
},
|
||||
desc: true,
|
||||
},
|
||||
],
|
||||
dimensions: [
|
||||
{
|
||||
@ -111,6 +120,7 @@ async function getPrevDayViewsChangePercent() {
|
||||
],
|
||||
});
|
||||
|
||||
console.log('response: ', JSON.stringify(response?.rows, null, 2));
|
||||
let viewsFromYesterday;
|
||||
let viewsFromDayBeforeYesterday;
|
||||
|
||||
@ -127,6 +137,7 @@ async function getPrevDayViewsChangePercent() {
|
||||
console.log('Page views are zero, so no percentage change');
|
||||
return '0';
|
||||
}
|
||||
console.table({ viewsFromYesterday, viewsFromDayBeforeYesterday });
|
||||
|
||||
const change = ((viewsFromYesterday - viewsFromDayBeforeYesterday) / viewsFromDayBeforeYesterday) * 100;
|
||||
return change.toFixed(2);
|
||||
|
Loading…
x
Reference in New Issue
Block a user