fix seed, stats, footer, etc

This commit is contained in:
vincanger 2023-11-30 16:16:18 +01:00
parent 646e33f881
commit 7850db18a3
20 changed files with 288 additions and 187 deletions

View File

@ -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=

View File

@ -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/' },
],
},
],

View 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

View File

@ -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

View File

@ -1,5 +0,0 @@
---
title: Email Sending
---
This reference explains when emails

View 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).

View File

@ -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 {

View 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;

View File

@ -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>

View File

@ -7,8 +7,8 @@ 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() {

View File

@ -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,76 +409,19 @@ 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 className='flex items-center justify-end mt-10 gap-20'>
<div>
<h3 className='text-sm font-semibold leading-6 text-gray-900'>Solutions</h3>
<h3 className='text-sm font-semibold leading-6 text-gray-900'>App</h3>
<ul role='list' className='mt-6 space-y-4'>
{footerNavigation.solutions.map((item) => (
{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}
@ -487,20 +430,6 @@ export default function LandingPage() {
))}
</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'>
@ -513,20 +442,6 @@ export default function LandingPage() {
))}
</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>
</div>
</footer>
</div>

View File

@ -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: '#' },
],
};

View File

@ -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 } },

View File

@ -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() {

View 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);
},
});

View File

@ -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,

View File

@ -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,

View File

@ -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;
};

View File

@ -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';

View File

@ -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);