mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-06-02 19:19:27 +02:00
referrer refactor and add sources stats
This commit is contained in:
parent
eb940e71b8
commit
c894ed102c
@ -7,6 +7,10 @@ export default defineConfig({
|
||||
integrations: [
|
||||
starlightBlog({
|
||||
title: 'The Best Blog Ever',
|
||||
sidebar: [
|
||||
{
|
||||
}
|
||||
],
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
|
@ -30,9 +30,7 @@ entity User {=psl
|
||||
credits Int @default(3)
|
||||
relatedObject RelatedObject[]
|
||||
externalAuthAssociations SocialLogin[]
|
||||
contactFormMessages ContactFormMessage[]
|
||||
referrer Referrer? @relation(fields: [referrerId], references: [id])
|
||||
referrerId Int?
|
||||
contactFormMessages ContactFormMessage[]
|
||||
psl=}
|
||||
```
|
||||
|
||||
|
61
main.wasp
61
main.wasp
@ -102,9 +102,7 @@ entity User {=psl
|
||||
credits Int @default(3)
|
||||
relatedObject RelatedObject[]
|
||||
externalAuthAssociations SocialLogin[]
|
||||
contactFormMessages ContactFormMessage[]
|
||||
referrer Referrer? @relation(fields: [referrerId], references: [id])
|
||||
referrerId Int?
|
||||
contactFormMessages ContactFormMessage[]
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
@ -138,21 +136,26 @@ entity ContactFormMessage {=psl
|
||||
psl=}
|
||||
|
||||
entity DailyStats {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
date DateTime @default(now()) @unique
|
||||
userCount Int @default(0)
|
||||
paidUserCount Int @default(0)
|
||||
userDelta Int @default(0)
|
||||
paidUserDelta Int @default(0)
|
||||
totalRevenue Float @default(0)
|
||||
totalProfit Float @default(0)
|
||||
id Int @id @default(autoincrement())
|
||||
date DateTime @default(now()) @unique
|
||||
totalViews Int @default(0)
|
||||
prevDayViewsChangePercent String @default("0")
|
||||
userCount Int @default(0)
|
||||
paidUserCount Int @default(0)
|
||||
userDelta Int @default(0)
|
||||
paidUserDelta Int @default(0)
|
||||
totalRevenue Float @default(0)
|
||||
totalProfit Float @default(0)
|
||||
sources PageViewSource[]
|
||||
psl=}
|
||||
|
||||
entity Referrer {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
name String @default("unknown") @unique
|
||||
count Int @default(0)
|
||||
users User[]
|
||||
entity PageViewSource {=psl
|
||||
date DateTime @default(now())
|
||||
name String @unique
|
||||
visitors Int
|
||||
dailyStats DailyStats? @relation(fields: [dailyStatsId], references: [id])
|
||||
dailyStatsId Int?
|
||||
@@id([date, name])
|
||||
psl=}
|
||||
|
||||
entity Logs {=psl
|
||||
@ -298,16 +301,6 @@ action updateUserById {
|
||||
entities: [User]
|
||||
}
|
||||
|
||||
action saveReferrer {
|
||||
fn: import { saveReferrer } from "@server/actions.js",
|
||||
entities: [Referrer]
|
||||
}
|
||||
|
||||
action UpdateUserReferrer {
|
||||
fn: import { updateUserReferrer } from "@server/actions.js",
|
||||
entities: [User, Referrer]
|
||||
}
|
||||
|
||||
// 📚 Queries
|
||||
|
||||
query getRelatedObjects {
|
||||
@ -320,21 +313,11 @@ query getDailyStats {
|
||||
entities: [User, DailyStats]
|
||||
}
|
||||
|
||||
query getReferrerStats {
|
||||
fn: import { getReferrerStats } from "@server/queries.js",
|
||||
entities: [User, Referrer]
|
||||
}
|
||||
|
||||
query getPaginatedUsers {
|
||||
fn: import { getPaginatedUsers } from "@server/queries.js",
|
||||
entities: [User]
|
||||
}
|
||||
|
||||
query getPlausibleStats {
|
||||
fn: import { getPlausibleStats } from "@server/queries.js",
|
||||
entities: [User, Logs]
|
||||
}
|
||||
|
||||
/*
|
||||
* 📡 These are custom Wasp API Endpoints. Use them for callbacks, webhooks, etc.
|
||||
* https://wasp-lang.dev/docs/advanced/apis
|
||||
@ -369,8 +352,8 @@ job dailyStats {
|
||||
},
|
||||
schedule: {
|
||||
// every hour
|
||||
cron: "0 * * * *"
|
||||
// cron: "* * * * *"
|
||||
// cron: "0 * * * *"
|
||||
cron: "* * * * *"
|
||||
},
|
||||
entities: [User, DailyStats, Logs]
|
||||
entities: [User, DailyStats, Logs, PageViewSource]
|
||||
}
|
||||
|
3
migrations/20231127110538_daily_page_views/migration.sql
Normal file
3
migrations/20231127110538_daily_page_views/migration.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DailyStats" ADD COLUMN "dailyPageViewsDelta" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "totalPageViews" INTEGER NOT NULL DEFAULT 0;
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `dailyPageViewsDelta` on the `DailyStats` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Integer`.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "DailyStats" ALTER COLUMN "dailyPageViewsDelta" SET DEFAULT 0,
|
||||
ALTER COLUMN "dailyPageViewsDelta" SET DATA TYPE INTEGER;
|
@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `dailyPageViewsDelta` on the `DailyStats` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `totalPageViews` on the `DailyStats` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "DailyStats" DROP COLUMN "dailyPageViewsDelta",
|
||||
DROP COLUMN "totalPageViews",
|
||||
ADD COLUMN "prevDayViewsChangePercent" TEXT NOT NULL DEFAULT '0',
|
||||
ADD COLUMN "totalViews" INTEGER NOT NULL DEFAULT 0;
|
15
migrations/20231127143949_page_view_source/migration.sql
Normal file
15
migrations/20231127143949_page_view_source/migration.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "PageViewSource" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"visitors" INTEGER NOT NULL,
|
||||
"dailyStatsId" INTEGER,
|
||||
|
||||
CONSTRAINT "PageViewSource_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PageViewSource_name_key" ON "PageViewSource"("name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PageViewSource" ADD CONSTRAINT "PageViewSource_dailyStatsId_fkey" FOREIGN KEY ("dailyStatsId") REFERENCES "DailyStats"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
2
migrations/20231127144953_page_view_source/migration.sql
Normal file
2
migrations/20231127144953_page_view_source/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "PageViewSource" ADD COLUMN "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
10
migrations/20231127145135_date_utc/migration.sql
Normal file
10
migrations/20231127145135_date_utc/migration.sql
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `date` on the `PageViewSource` table. All the data in the column will be lost.
|
||||
- Added the required column `dateUTC` to the `PageViewSource` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PageViewSource" DROP COLUMN "date",
|
||||
ADD COLUMN "dateUTC" TEXT NOT NULL;
|
9
migrations/20231127145351_date/migration.sql
Normal file
9
migrations/20231127145351_date/migration.sql
Normal file
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `dateUTC` on the `PageViewSource` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PageViewSource" DROP COLUMN "dateUTC",
|
||||
ADD COLUMN "dat" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
13
migrations/20231127145440_unique_date/migration.sql
Normal file
13
migrations/20231127145440_unique_date/migration.sql
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `dat` on the `PageViewSource` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[date]` on the table `PageViewSource` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PageViewSource" DROP COLUMN "dat",
|
||||
ADD COLUMN "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PageViewSource_date_key" ON "PageViewSource"("date");
|
11
migrations/20231127151321_composite_key/migration.sql
Normal file
11
migrations/20231127151321_composite_key/migration.sql
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `PageViewSource` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- You are about to drop the column `id` on the `PageViewSource` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PageViewSource" DROP CONSTRAINT "PageViewSource_pkey",
|
||||
DROP COLUMN "id",
|
||||
ADD CONSTRAINT "PageViewSource_pkey" PRIMARY KEY ("date", "name");
|
2
migrations/20231127151444_f/migration.sql
Normal file
2
migrations/20231127151444_f/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "PageViewSource_date_key";
|
15
migrations/20231127161704_/migration.sql
Normal file
15
migrations/20231127161704_/migration.sql
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `referrerId` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the `Referrer` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "User" DROP CONSTRAINT "User_referrerId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "referrerId";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Referrer";
|
@ -2,11 +2,8 @@ import './Main.css';
|
||||
import AppNavBar from './components/AppNavBar';
|
||||
import { useMemo, useEffect, ReactNode } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useReferrer, UNKOWN_REFERRER } from './hooks/useReferrer';
|
||||
import useAuth from '@wasp/auth/useAuth';
|
||||
import updateCurrentUser from '@wasp/actions/updateCurrentUser'; // TODO fix
|
||||
import updateUserReferrer from '@wasp/actions/UpdateUserReferrer';
|
||||
import saveReferrer from '@wasp/actions/saveReferrer';
|
||||
|
||||
/**
|
||||
* use this component to wrap all child components
|
||||
@ -15,7 +12,6 @@ import saveReferrer from '@wasp/actions/saveReferrer';
|
||||
export default function App({ children }: { children: ReactNode }) {
|
||||
const location = useLocation();
|
||||
const { data: user } = useAuth();
|
||||
const [referrer, setReferrer] = useReferrer();
|
||||
|
||||
const shouldDisplayAppNavBar = useMemo(() => {
|
||||
return location.pathname !== '/' && location.pathname !== '/login' && location.pathname !== '/signup';
|
||||
@ -34,26 +30,6 @@ export default function App({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (referrer && referrer.ref !== UNKOWN_REFERRER && !referrer.isSavedInDB) {
|
||||
saveReferrer({ name: referrer.ref });
|
||||
setReferrer({
|
||||
...referrer,
|
||||
isSavedInDB: true,
|
||||
});
|
||||
}
|
||||
}, [referrer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && referrer && !referrer.isSavedToUser && referrer.ref !== UNKOWN_REFERRER) {
|
||||
updateUserReferrer({ name: referrer.ref });
|
||||
setReferrer({
|
||||
...referrer,
|
||||
isSavedToUser: true,
|
||||
});
|
||||
}
|
||||
}, [user, referrer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.hash) {
|
||||
const id = location.hash.replace('#', '');
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { useQuery } from '@wasp/queries';
|
||||
import getReferrerStats from '@wasp/queries/getReferrerStats';
|
||||
|
||||
// We're using a simple, in-house analytics system that tracks referrers and page views.
|
||||
// You could instead set up Google Analytics or Plausible and use their API for more detailed stats.
|
||||
const ReferrerTable = () => {
|
||||
const { data: referrers, isLoading: isReferrersLoading, error: referrersError } = useQuery(getReferrerStats);
|
||||
|
||||
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 Referrers</h4>
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<div className='grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 sm:grid-cols-4'>
|
||||
<div className='p-2.5 xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Source</h5>
|
||||
</div>
|
||||
<div className='p-2.5 text-center xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Visitors</h5>
|
||||
</div>
|
||||
<div className='p-2.5 text-center xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Conversion</h5>
|
||||
<span className='text-xs font-normal text-gray-600 whitespace-nowrap'>% of visitors that register</span>
|
||||
</div>
|
||||
<div className='hidden p-2.5 text-center sm:block xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Sales</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{referrers &&
|
||||
referrers.length > 0 &&
|
||||
referrers.map((ref) => (
|
||||
<div className='grid grid-cols-3 border-b border-stroke dark:border-strokedark sm:grid-cols-4'>
|
||||
<div className='flex items-center gap-3 p-2.5 xl:p-5'>
|
||||
<p className='text-black dark:text-white'>{ref.name}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center p-2.5 xl:p-5'>
|
||||
<p className='text-black dark:text-white'>{ref.count}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center p-2.5 xl:p-5'>
|
||||
<p className='text-meta-3'>{ref.users.length > 0 ? Math.round((ref.users.length / ref.count)*100) : '0'}%</p>
|
||||
</div>
|
||||
|
||||
<div className='hidden items-center justify-center p-2.5 sm:flex xl:p-5'>
|
||||
<p className='text-black dark:text-white'>--</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReferrerTable;
|
@ -11,7 +11,7 @@ const options: ApexOptions = {
|
||||
},
|
||||
colors: ['#3C50E0', '#80CAEE'],
|
||||
chart: {
|
||||
fontFamily: 'Satoshi, sans-serif',
|
||||
fontFamily: 'Satoshi, sans-serif',
|
||||
height: 335,
|
||||
type: 'area',
|
||||
dropShadow: {
|
||||
@ -175,18 +175,22 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
|
||||
}, [dailyRevenueArray]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('ooptions categories: ', options?.xaxis?.categories);
|
||||
console.log('days of week arr: ', daysOfWeekArr);
|
||||
if (!!daysOfWeekArr && daysOfWeekArr?.length > 0) {
|
||||
if (!!daysOfWeekArr && daysOfWeekArr?.length > 0 && !!dailyRevenueArray && dailyRevenueArray?.length > 0) {
|
||||
setChartOptions({
|
||||
...options,
|
||||
xaxis: {
|
||||
...options.xaxis,
|
||||
categories: daysOfWeekArr,
|
||||
},
|
||||
yaxis: {
|
||||
...options.yaxis,
|
||||
// get the min & max values to the neareast hundred
|
||||
max: Math.ceil(Math.max(...dailyRevenueArray) / 100) * 100,
|
||||
min: Math.floor(Math.min(...dailyRevenueArray) / 100) * 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [daysOfWeekArr]);
|
||||
}, [daysOfWeekArr, dailyRevenueArray]);
|
||||
|
||||
return (
|
||||
<div className='col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8'>
|
||||
|
49
src/client/admin/components/SourcesTable.tsx
Normal file
49
src/client/admin/components/SourcesTable.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { 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'>
|
||||
<div className='grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 '>
|
||||
<div className='p-2.5 xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Source</h5>
|
||||
</div>
|
||||
<div className='p-2.5 text-center xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Visitors</h5>
|
||||
</div>
|
||||
<div className='hidden p-2.5 text-center sm:block xl:p-5'>
|
||||
<h5 className='text-sm font-medium uppercase xsm:text-base'>Sales</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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'>
|
||||
<p className='text-black dark:text-white'>{source.name}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center p-2.5 xl:p-5'>
|
||||
<p className='text-black dark:text-white'>{source.visitors}</p>
|
||||
</div>
|
||||
|
||||
<div className='hidden items-center justify-center p-2.5 sm:flex xl:p-5'>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcesTable;
|
@ -19,7 +19,7 @@ const SwitcherOne = ({ user, updateUserById}: { user?: Partial<User>, updateUser
|
||||
/>
|
||||
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
|
||||
<div
|
||||
className={`absolute left-1 top-1 h-6 w-6 rounded-full bg-white transition ${
|
||||
className={`absolute left-1 top-1 h-6 w-6 rounded-full bg-white dark:bg-gray-400 transition ${
|
||||
enabled && '!right-1 !translate-x-full !bg-primary dark:!bg-white'
|
||||
}`}
|
||||
></div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { UpArrow, DownArrow } from "../images/icon/icons-arrows";
|
||||
|
||||
type PageViewsStats = {
|
||||
totalPageViews: string | undefined;
|
||||
dailyChangePercentage: string | undefined;
|
||||
totalPageViews: number | undefined;
|
||||
prevDayViewsChangePercent: string | undefined;
|
||||
}
|
||||
|
||||
const TotalPageViewsCard = ({ totalPageViews, dailyChangePercentage } : PageViewsStats ) => {
|
||||
const TotalPageViewsCard = ({ totalPageViews, prevDayViewsChangePercent } : PageViewsStats ) => {
|
||||
|
||||
const isDeltaPositive = parseInt(dailyChangePercentage || '') > 0;
|
||||
const isDeltaPositive = parseInt(prevDayViewsChangePercent || '') > 0;
|
||||
|
||||
return (
|
||||
<div className='rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark'>
|
||||
@ -37,11 +37,11 @@ const TotalPageViewsCard = ({ totalPageViews, dailyChangePercentage } : PageView
|
||||
<span className='text-sm font-medium'>Total page views</span>
|
||||
</div>
|
||||
|
||||
{dailyChangePercentage && parseInt(dailyChangePercentage) !== 0 && (
|
||||
{prevDayViewsChangePercent && parseInt(prevDayViewsChangePercent) !== 0 && (
|
||||
<span
|
||||
className={`flex items-center gap-1 text-sm font-medium ${isDeltaPositive ? 'text-meta-3' : 'text-meta-5'}`}
|
||||
>
|
||||
{dailyChangePercentage}%{parseInt(dailyChangePercentage) > 0 ? <UpArrow /> : <DownArrow />}
|
||||
{prevDayViewsChangePercent}%{parseInt(prevDayViewsChangePercent) > 0 ? <UpArrow /> : <DownArrow />}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
@ -9,18 +9,15 @@ const TotalRevenueCard = ({dailyStats, weeklyStats, isLoading}: DailyStatsProps)
|
||||
}, [weeklyStats]);
|
||||
|
||||
const deltaPercentage = useMemo(() => {
|
||||
if ( !weeklyStats || isLoading) return;
|
||||
if ( !weeklyStats || weeklyStats.length < 2 || isLoading) return;
|
||||
if ( weeklyStats[1]?.totalRevenue === 0 || weeklyStats[0]?.totalRevenue === 0 ) return 0;
|
||||
|
||||
weeklyStats.sort((a, b) => b.id - a.id);
|
||||
console.log('weeklyStats[1]?.totalRevenue; ', !!weeklyStats && weeklyStats)
|
||||
const percentage = ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
|
||||
return Math.floor(percentage);
|
||||
}, [weeklyStats]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('deltaPercentage; ', deltaPercentage)
|
||||
console.log('weeklyStats; ', weeklyStats)
|
||||
}, [deltaPercentage])
|
||||
|
||||
return (
|
||||
<div className='rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark'>
|
||||
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-meta-2 dark:bg-meta-4'>
|
||||
|
@ -32,8 +32,8 @@ const UsersTable = () => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
|
||||
<div className='flex-col flex items-start justify-between p-6 gap-3 w-full '>
|
||||
<span className='text-sm font-semibold text-gray-700'>Filters:</span>
|
||||
<div className='flex-col flex items-start justify-between p-6 gap-3 w-full bg-gray-100/40 dark:bg-gray-700/50'>
|
||||
<span className='text-sm font-medium'>Filters:</span>
|
||||
<div className='flex items-center justify-between gap-3 w-full px-2'>
|
||||
<div className='relative flex items-center gap-3 '>
|
||||
<label htmlFor='email-filter' className='block text-sm text-gray-700 dark:text-white'>
|
||||
@ -46,18 +46,18 @@ const UsersTable = () => {
|
||||
onChange={(e) => {
|
||||
setEmail(e.currentTarget.value);
|
||||
}}
|
||||
className='rounded border border-stroke bg-transparent py-2 px-5 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
|
||||
className='rounded border border-stroke py-2 px-5 bg-white outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
|
||||
/>
|
||||
<label htmlFor='status-filter' className='block text-sm ml-2 text-gray-700 dark:text-white'>
|
||||
status:
|
||||
</label>
|
||||
<div className='flex-grow relative z-20 rounded border border-stroke pr-8 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<div className='flex-grow relative z-20 rounded border border-stroke pr-8 outline-none bg-white transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
|
||||
<div className='flex items-center'>
|
||||
{!!statusOptions && statusOptions.length > 0 ? (
|
||||
statusOptions.map((opt, idx) => (
|
||||
<span
|
||||
key={opt}
|
||||
className='z-30 flex items-center bg-transparent my-1 mx-2 py-1 px-2 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
|
||||
className='z-30 flex items-center my-1 mx-2 py-1 px-2 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
|
||||
>
|
||||
{opt}
|
||||
<span
|
||||
@ -87,7 +87,7 @@ const UsersTable = () => {
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className='bg-transparent text-gray-500 py-2 px-5 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'>
|
||||
<span className='bg-white text-gray-500 py-2 px-5 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'>
|
||||
Select Status Filters
|
||||
</span>
|
||||
)}
|
||||
@ -106,7 +106,7 @@ const UsersTable = () => {
|
||||
}}
|
||||
name='status-filter'
|
||||
id='status-filter'
|
||||
className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'
|
||||
className='absolute top-0 left-0 z-20 h-full w-full bg-white opacity-0'
|
||||
>
|
||||
<option value=''>Select filters</option>
|
||||
{['past_due', 'canceled', 'active'].map((status) => {
|
||||
@ -141,7 +141,7 @@ const UsersTable = () => {
|
||||
setHasPaidFilter(e.target.value === 'true');
|
||||
}
|
||||
}}
|
||||
className='relative z-20 w-full appearance-none rounded border border-stroke bg-transparent p-2 pl-4 pr-8 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'
|
||||
className='relative z-20 w-full appearance-none rounded border border-stroke bg-white p-2 pl-4 pr-8 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'
|
||||
>
|
||||
<option value='both'>both</option>
|
||||
<option value='true'>true</option>
|
||||
@ -162,6 +162,7 @@ const UsersTable = () => {
|
||||
}}
|
||||
className='rounded-md border-1 border-stroke bg-transparent px-4 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
|
||||
/>
|
||||
<span className='text-md text-black dark:text-white'> / {data?.totalPages} </span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -3,11 +3,10 @@ import TotalPageViewsCard from '../../components/TotalPaidViewsCard';
|
||||
import TotalPayingUsersCard from '../../components/TotalPayingUsersCard';
|
||||
import TotalRevenueCard from '../../components/TotalRevenueCard';
|
||||
import RevenueAndProfitChart from '../../components/RevenueAndProfitChart';
|
||||
import ReferrerTable from '../../components/ReferrerTable';
|
||||
import SourcesTable from '../../components/SourcesTable';
|
||||
import DefaultLayout from '../../layout/DefaultLayout';
|
||||
import { useQuery } from '@wasp/queries';
|
||||
import getDailyStats from '@wasp/queries/getDailyStats';
|
||||
import getPlausibleStats from '@wasp/queries/getPlausibleStats';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import type { User } from '@wasp/entities';
|
||||
|
||||
@ -18,12 +17,14 @@ const ECommerce = ({ user} : { user: User }) => {
|
||||
}
|
||||
|
||||
const { data: stats, isLoading, error } = useQuery(getDailyStats);
|
||||
const { data: plausibleStats, isLoading: isPlausibleLoading, error: plausibleError } = useQuery(getPlausibleStats);
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5'>
|
||||
<TotalPageViewsCard totalPageViews={plausibleStats?.totalPageViews} dailyChangePercentage={plausibleStats?.dailyChangePercentage} />
|
||||
<TotalPageViewsCard
|
||||
totalPageViews={stats?.dailyStats.totalViews}
|
||||
prevDayViewsChangePercent={stats?.dailyStats.prevDayViewsChangePercent}
|
||||
/>
|
||||
<TotalRevenueCard dailyStats={stats?.dailyStats} weeklyStats={stats?.weeklyStats} isLoading={isLoading} />
|
||||
<TotalPayingUsersCard dailyStats={stats?.dailyStats} isLoading={isLoading} />
|
||||
<TotalSignupsCard dailyStats={stats?.dailyStats} isLoading={isLoading} />
|
||||
@ -33,7 +34,7 @@ const ECommerce = ({ user} : { user: User }) => {
|
||||
<RevenueAndProfitChart weeklyStats={stats?.weeklyStats} isLoading={isLoading} />
|
||||
|
||||
<div className='col-span-12 xl:col-span-8'>
|
||||
<ReferrerTable />
|
||||
<SourcesTable sources={stats?.dailyStats?.sources} />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
|
@ -1,32 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import useLocalStorage from './useLocalStorage';
|
||||
|
||||
const REFERRER_KEY = 'ref';
|
||||
export const UNKOWN_REFERRER = 'unknown';
|
||||
|
||||
export function useReferrer() {
|
||||
const history = useHistory();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const refValue = urlParams.get(REFERRER_KEY);
|
||||
|
||||
const values = {
|
||||
[REFERRER_KEY]: refValue || UNKOWN_REFERRER,
|
||||
isSavedInDB: false,
|
||||
isSavedToUser: false,
|
||||
}
|
||||
|
||||
const [referrer, setReferrer] = useLocalStorage(REFERRER_KEY, values);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('referrer', referrer);
|
||||
if (!!refValue && refValue !== UNKOWN_REFERRER) {
|
||||
setReferrer(values);
|
||||
history.replace({
|
||||
search: '',
|
||||
});
|
||||
}
|
||||
}, [referrer]);
|
||||
|
||||
return [referrer, setReferrer] as const;
|
||||
}
|
@ -4,7 +4,7 @@ import HttpError from '@wasp/core/HttpError.js';
|
||||
import type { RelatedObject, User } from '@wasp/entities';
|
||||
import type { GenerateGptResponse, StripePayment } from '@wasp/actions/types';
|
||||
import type { StripePaymentResult, OpenAIResponse } from './types';
|
||||
import { UpdateCurrentUser, SaveReferrer, UpdateUserReferrer, UpdateUserById } from '@wasp/actions/types';
|
||||
import { UpdateCurrentUser, UpdateUserById } from '@wasp/actions/types';
|
||||
import { fetchStripeCustomer, createStripeCheckoutSession } from './stripeUtils.js';
|
||||
import { TierIds } from '@wasp/shared/const.js';
|
||||
|
||||
@ -167,38 +167,3 @@ export const updateCurrentUser: UpdateCurrentUser<Partial<User>, User> = async (
|
||||
data: user,
|
||||
});
|
||||
};
|
||||
|
||||
export const saveReferrer: SaveReferrer<{ name: string }, void> = async ({ name }, context) => {
|
||||
await context.entities.Referrer.upsert({
|
||||
where: {
|
||||
name,
|
||||
},
|
||||
create: {
|
||||
name,
|
||||
count: 1,
|
||||
},
|
||||
update: {
|
||||
count: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUserReferrer: UpdateUserReferrer<{ name: string }, void> = async ({ name }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
await context.entities.User.update({
|
||||
where: {
|
||||
id: context.user.id,
|
||||
},
|
||||
data: {
|
||||
referrer: {
|
||||
connect: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,17 +1,18 @@
|
||||
import HttpError from '@wasp/core/HttpError.js';
|
||||
import { getTotalPageViews, calculateDailyChangePercentage } from './analyticsUtils.js';
|
||||
import type { DailyStats, RelatedObject, Referrer, User } from '@wasp/entities';
|
||||
import type { DailyStats, RelatedObject, User, PageViewSource } from '@wasp/entities';
|
||||
import type {
|
||||
GetRelatedObjects,
|
||||
GetDailyStats,
|
||||
GetReferrerStats,
|
||||
GetPaginatedUsers,
|
||||
GetPlausibleStats,
|
||||
} from '@wasp/queries/types';
|
||||
|
||||
type DailyStatsWithSources = DailyStats & {
|
||||
sources: PageViewSource[];
|
||||
};
|
||||
|
||||
type DailyStatsValues = {
|
||||
dailyStats: DailyStats;
|
||||
weeklyStats: DailyStats[];
|
||||
dailyStats: DailyStatsWithSources;
|
||||
weeklyStats: DailyStatsWithSources[];
|
||||
};
|
||||
|
||||
export const getRelatedObjects: GetRelatedObjects<void, RelatedObject[]> = async (args, context) => {
|
||||
@ -35,38 +36,24 @@ export const getDailyStats: GetDailyStats<void, DailyStatsValues> = async (_args
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
include: {
|
||||
sources: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('dailyStats: ', dailyStats)
|
||||
|
||||
const weeklyStats = await context.entities.DailyStats.findMany({
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
take: 7,
|
||||
});
|
||||
|
||||
return { dailyStats, weeklyStats };
|
||||
};
|
||||
|
||||
type ReferrerWithSanitizedUsers = Referrer & {
|
||||
users: Pick<User, 'id' | 'email' | 'hasPaid' | 'subscriptionStatus'>[];
|
||||
};
|
||||
|
||||
export const getReferrerStats: GetReferrerStats<void, ReferrerWithSanitizedUsers[]> = async (args, context) => {
|
||||
const referrers = await context.entities.Referrer.findMany({
|
||||
include: {
|
||||
users: true,
|
||||
sources: true,
|
||||
},
|
||||
});
|
||||
|
||||
return referrers.map((referrer) => ({
|
||||
...referrer,
|
||||
users: referrer.users.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
hasPaid: user.hasPaid,
|
||||
subscriptionStatus: user.subscriptionStatus,
|
||||
})),
|
||||
}));
|
||||
return { dailyStats, weeklyStats };
|
||||
};
|
||||
|
||||
type GetPaginatedUsersInput = {
|
||||
@ -131,22 +118,4 @@ export const getPaginatedUsers: GetPaginatedUsers<GetPaginatedUsersInput, GetPag
|
||||
users: queryResults,
|
||||
totalPages,
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: move this and analyticsUtils to Cron Job
|
||||
export const getPlausibleStats: GetPlausibleStats<
|
||||
void,
|
||||
{ totalPageViews: string | undefined; dailyChangePercentage: string | undefined }
|
||||
> = async (_args, context) => {
|
||||
if (!context.user?.isAdmin) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const totalPageViews = (await getTotalPageViews()).toString();
|
||||
const dailyChangePercentage = await calculateDailyChangePercentage();
|
||||
|
||||
return {
|
||||
totalPageViews,
|
||||
dailyChangePercentage,
|
||||
};
|
||||
};
|
||||
};
|
@ -1,27 +1,9 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
import type { User, Referrer } from '@wasp/entities';
|
||||
import type { User } from '@wasp/entities';
|
||||
|
||||
// in a terminal window run `wasp db seed` to seed your dev database with this data
|
||||
|
||||
const referrerArr: Referrer[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'product-hunt',
|
||||
count: 27,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'twitter',
|
||||
count: 26,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'linkedin',
|
||||
count: 25,
|
||||
},
|
||||
];
|
||||
|
||||
let prevUserId = 0;
|
||||
export function createRandomUser(): Partial<User> {
|
||||
const user: Partial<User> = {
|
||||
@ -41,7 +23,6 @@ export function createRandomUser(): Partial<User> {
|
||||
subscriptionStatus: faker.helpers.arrayElement(['active', 'canceled', 'past_due']),
|
||||
datePaid: faker.date.recent(),
|
||||
credits: faker.number.int({ min: 0, max: 3 }),
|
||||
referrerId: faker.number.int({ min: 1, max: 3 }),
|
||||
};
|
||||
return user;
|
||||
}
|
||||
@ -52,13 +33,6 @@ const USERS: Partial<User>[] = faker.helpers.multiple(createRandomUser, {
|
||||
|
||||
export async function devSeedUsers(prismaClient: PrismaClient) {
|
||||
try {
|
||||
await Promise.all(
|
||||
referrerArr.map(async (referrer) => {
|
||||
await prismaClient.referrer.create({
|
||||
data: referrer,
|
||||
});
|
||||
})
|
||||
);
|
||||
await Promise.all(
|
||||
USERS.map(async (user) => {
|
||||
await prismaClient.user.create({
|
||||
|
@ -15,6 +15,13 @@ type PageViewsResult = {
|
||||
};
|
||||
};
|
||||
|
||||
type PageViewSourcesResult = {
|
||||
results: [{
|
||||
source: string;
|
||||
visitors: number;
|
||||
}];
|
||||
};
|
||||
|
||||
export async function getTotalPageViews() {
|
||||
const response = await fetch(
|
||||
`${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&metrics=pageviews`,
|
||||
@ -26,12 +33,12 @@ export async function getTotalPageViews() {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const json = await response.json() as PageViewsResult;
|
||||
const json = (await response.json()) as PageViewsResult;
|
||||
|
||||
return json.results.pageviews.value;
|
||||
}
|
||||
|
||||
export async function calculateDailyChangePercentage() {
|
||||
export async function getPrevDayViewsChangePercent() {
|
||||
// Calculate today, yesterday, and the day before yesterday's dates
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today.setDate(today.getDate() - 1)).toISOString().split('T')[0];
|
||||
@ -42,7 +49,12 @@ export async function calculateDailyChangePercentage() {
|
||||
const pageViewsYesterday = await getPageviewsForDate(yesterday);
|
||||
const pageViewsDayBeforeYesterday = await getPageviewsForDate(dayBeforeYesterday);
|
||||
|
||||
console.table({ pageViewsYesterday, pageViewsDayBeforeYesterday, typeY: typeof pageViewsYesterday, typeDBY: typeof pageViewsDayBeforeYesterday })
|
||||
console.table({
|
||||
pageViewsYesterday,
|
||||
pageViewsDayBeforeYesterday,
|
||||
typeY: typeof pageViewsYesterday,
|
||||
typeDBY: typeof pageViewsDayBeforeYesterday,
|
||||
});
|
||||
|
||||
let change = 0;
|
||||
if (pageViewsYesterday === 0 || pageViewsDayBeforeYesterday === 0) {
|
||||
@ -52,7 +64,7 @@ export async function calculateDailyChangePercentage() {
|
||||
}
|
||||
|
||||
console.log(`Daily change in page views percentage: ${change.toFixed(2)}%`);
|
||||
return change.toFixed(2); // Limit the number to two decimal places
|
||||
return change.toFixed(2);
|
||||
} catch (error) {
|
||||
console.error('Error calculating daily change percentage:', error);
|
||||
}
|
||||
@ -67,6 +79,19 @@ async function getPageviewsForDate(date: string) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json() as PageViewsResult;
|
||||
const data = (await response.json()) as PageViewsResult;
|
||||
return data.results.pageviews.value;
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
const url = `${PLAUSIBLE_BASE_URL}/v1/stats/breakdown?site_id=${PLAUSIBLE_SITE_ID}&property=visit:source&metrics=visitors`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json() as PageViewSourcesResult;
|
||||
return data.results;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import type { DailyStats } from '@wasp/jobs/dailyStats';
|
||||
import Stripe from 'stripe';
|
||||
import { getTotalPageViews, getPrevDayViewsChangePercent, getSources } from './analyticsUtils.js';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
apiVersion: '2022-11-15', // TODO find out where this is in the Stripe dashboard and document
|
||||
@ -40,8 +41,9 @@ export const calculateDailyStats: DailyStats<never, void> = async (_args, contex
|
||||
userDelta -= yesterdaysStats.userCount;
|
||||
paidUserDelta -= yesterdaysStats.paidUserCount;
|
||||
}
|
||||
|
||||
const newRunningTotal = await calculateTotalRevenue(context);
|
||||
|
||||
const totalRevenue = await fetchTotalStripeRevenue();
|
||||
const { totalViews, prevDayViewsChangePercent } = await getDailyPageviews();
|
||||
|
||||
const newDailyStat = await context.entities.DailyStats.upsert({
|
||||
where: {
|
||||
@ -49,21 +51,47 @@ export const calculateDailyStats: DailyStats<never, void> = async (_args, contex
|
||||
},
|
||||
create: {
|
||||
date: nowUTC,
|
||||
totalViews,
|
||||
prevDayViewsChangePercent: prevDayViewsChangePercent || '0',
|
||||
userCount,
|
||||
paidUserCount,
|
||||
userDelta,
|
||||
paidUserDelta,
|
||||
totalRevenue: newRunningTotal,
|
||||
totalRevenue,
|
||||
},
|
||||
update: {
|
||||
totalViews,
|
||||
prevDayViewsChangePercent: prevDayViewsChangePercent || '0' ,
|
||||
userCount,
|
||||
paidUserCount,
|
||||
userDelta,
|
||||
paidUserDelta,
|
||||
totalRevenue: newRunningTotal,
|
||||
totalRevenue,
|
||||
},
|
||||
});
|
||||
|
||||
const sources = await getSources();
|
||||
|
||||
for (const source of sources) {
|
||||
await context.entities.PageViewSource.upsert({
|
||||
where: {
|
||||
date_name: {
|
||||
date: nowUTC,
|
||||
name: source.source,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
date: nowUTC,
|
||||
name: source.source,
|
||||
visitors: source.visitors,
|
||||
dailyStatsId: newDailyStat.id,
|
||||
},
|
||||
update: {
|
||||
visitors: source.visitors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.table({ newDailyStat })
|
||||
|
||||
} catch (error: any) {
|
||||
@ -77,75 +105,46 @@ export const calculateDailyStats: DailyStats<never, void> = async (_args, contex
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchDailyStripeRevenue() {
|
||||
const startOfDayUTC = new Date(Date.now());
|
||||
startOfDayUTC.setHours(0, 0, 0, 0); // Sets to beginning of day
|
||||
const startOfDayTimestamp = Math.floor(startOfDayUTC.getTime() / 1000); // Convert to Unix timestamp in seconds
|
||||
async function fetchTotalStripeRevenue() {
|
||||
let totalRevenue = 0;
|
||||
let params: Stripe.BalanceTransactionListParams = {
|
||||
limit: 100,
|
||||
// created: {
|
||||
// gte: startTimestamp,
|
||||
// lt: endTimestamp
|
||||
// },
|
||||
type: 'charge',
|
||||
};
|
||||
|
||||
const endOfDayUTC = new Date();
|
||||
endOfDayUTC.setHours(23, 59, 59, 999); // Sets to end of day
|
||||
const endOfDayTimestamp = Math.floor(endOfDayUTC.getTime() / 1000); // Convert to Unix timestamp in seconds
|
||||
let hasMore = true;
|
||||
while (hasMore) {
|
||||
const balanceTransactions = await stripe.balanceTransactions.list(params);
|
||||
|
||||
let nextPageCursor = undefined;
|
||||
const allPayments = [] as Stripe.Invoice[];
|
||||
for (const transaction of balanceTransactions.data) {
|
||||
if (transaction.type === 'charge') {
|
||||
totalRevenue += transaction.amount;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// Stripe allows searching for invoices by date range via their Query Language
|
||||
// If there are more than 100 invoices in a day, we need to paginate through them
|
||||
const params = {
|
||||
query: `created>=${startOfDayTimestamp} AND created<=${endOfDayTimestamp} AND status:"paid"`,
|
||||
limit: 100,
|
||||
page: nextPageCursor,
|
||||
if (balanceTransactions.has_more) {
|
||||
// Set the starting point for the next iteration to the last object fetched
|
||||
params.starting_after = balanceTransactions.data[balanceTransactions.data.length - 1].id;
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Revenue is in cents so we convert to dollars (or your main currency unit)
|
||||
const formattedRevenue = (totalRevenue / 100)
|
||||
return formattedRevenue;
|
||||
}
|
||||
|
||||
async function getDailyPageviews() {
|
||||
const totalViews = await getTotalPageViews()
|
||||
const prevDayViewsChangePercent = await getPrevDayViewsChangePercent();
|
||||
|
||||
return {
|
||||
totalViews,
|
||||
prevDayViewsChangePercent,
|
||||
};
|
||||
const payments = await stripe.invoices.search(params);
|
||||
|
||||
if (payments.next_page) {
|
||||
nextPageCursor = payments.next_page;
|
||||
}
|
||||
|
||||
console.log('\n\nstripe invoice payments: ', payments, '\n\n');
|
||||
|
||||
payments.data.forEach((invoice) => allPayments.push(invoice));
|
||||
|
||||
if (!payments.has_more) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dailyTotalInCents = allPayments.reduce((total, invoice) => {
|
||||
return total + invoice.amount_paid;
|
||||
}, 0);
|
||||
|
||||
return dailyTotalInCents;
|
||||
}
|
||||
|
||||
async function calculateTotalRevenue(context: any) {
|
||||
const revenueInCents = await fetchDailyStripeRevenue();
|
||||
|
||||
const revenueInDollars = revenueInCents / 100;
|
||||
|
||||
// we use UTC time to avoid issues with local timezones
|
||||
const nowUTC = new Date(Date.now());
|
||||
|
||||
// Set the time component to midnight in UTC
|
||||
// This way we can pass the Date object directly to Prisma
|
||||
// without having to convert it to a string
|
||||
nowUTC.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
// Get yesterday's date by subtracting one day
|
||||
const yesterdayUTC = new Date(nowUTC);
|
||||
yesterdayUTC.setUTCDate(yesterdayUTC.getUTCDate() - 1);
|
||||
|
||||
const lastTotalEntry = await context.entities.DailyStats.findUnique({
|
||||
where: {
|
||||
date: yesterdayUTC, // Pass the Date object directly, not as a string
|
||||
},
|
||||
});
|
||||
|
||||
let newRunningTotal = revenueInDollars;
|
||||
if (lastTotalEntry) {
|
||||
newRunningTotal += lastTotalEntry.totalRevenue;
|
||||
}
|
||||
|
||||
return newRunningTotal;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user