update ref and users table
@ -1,4 +1,6 @@
|
||||
# NOTE: if you setup your DB using `wasp start db` then you DO NOT need to add a DATABASE_URL env.
|
||||
# NOTE: you can let Wasp set up your Postgres DB by running `wasp start db` in a separate terminal window.
|
||||
# then, in a new terminal window, run `wasp db migrate-dev` and finally `wasp start`.
|
||||
# If you use `wasp start db` then you DO NOT need to add a DATABASE_URL env variable here.
|
||||
# DATABASE_URL=
|
||||
|
||||
# for testing, go to https://dashboard.stripe.com/test/apikeys and get a test stripe key that starts with "sk_test_..."
|
||||
@ -10,10 +12,10 @@ SUBSCRIPTION_PRICE_ID=
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# (OPTIONAL) get your openai api key at https://platform.openai.com/account
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# get your sendgrid api key at https://app.sendgrid.com/settings/api_keys
|
||||
SENDGRID_API_KEY=
|
||||
# if not explicitly set to true, emails be logged to console but not actually sent
|
||||
SEND_EMAILS_IN_DEVELOPMENT=true
|
||||
|
||||
# (OPTIONAL) get your openai api key at https://platform.openai.com/account
|
||||
OPENAI_API_KEY=
|
74
main.wasp
@ -5,9 +5,13 @@ app SaaSTemplate {
|
||||
title: "My SaaS App",
|
||||
head: [
|
||||
"<meta property='og:type' content='website' />",
|
||||
"<meta property='og:url' content='https://mySaaSapp.com' />",
|
||||
"<meta property='og:url' content='https://mySaaSapp.com' />", // TODO change url
|
||||
"<meta property='og:description' content='I made a SaaS App. Buy my stuff.' />",
|
||||
"<meta property='og:image' content='src/client/static/image.png' />",
|
||||
"<meta name='twitter:image' content='https://mySaaSapp.com/gptsaastemplate.png' />", // TODO change url and image
|
||||
"<meta name='twitter:image:width' content='800' />",
|
||||
"<meta name='twitter:image:height' content='400' />",
|
||||
"<meta name='twitter:card' content='summary_large_image' />",
|
||||
// you can put your google analytics script here, too!
|
||||
],
|
||||
// 🔐 Auth out of the box! https://wasp-lang.dev/docs/auth/overview
|
||||
@ -38,7 +42,11 @@ app SaaSTemplate {
|
||||
onAuthFailedRedirectTo: "/",
|
||||
},
|
||||
db: {
|
||||
system: PostgreSQL
|
||||
system: PostgreSQL,
|
||||
seeds: [
|
||||
import { devSeedUsers } from "@server/scripts/usersSeed.js",
|
||||
|
||||
]
|
||||
},
|
||||
client: {
|
||||
rootComponent: import App from "@client/App",
|
||||
@ -48,7 +56,7 @@ app SaaSTemplate {
|
||||
defaultFrom: {
|
||||
name: "SaaS App",
|
||||
// make sure this address is the same you registered your SendGrid or MailGun account with!
|
||||
email: "vince@wasp-lang.dev"
|
||||
email: "vince@wasp-lang.dev" // TODO change to generic email before pushing to github
|
||||
},
|
||||
},
|
||||
dependencies: [
|
||||
@ -66,7 +74,7 @@ app SaaSTemplate {
|
||||
("react-apexcharts", "^1.4.1"),
|
||||
("apexcharts", "^3.41.0"),
|
||||
("headlessui", "^0.0.0"),
|
||||
|
||||
("@faker-js/faker", "8.3.1")
|
||||
],
|
||||
}
|
||||
|
||||
@ -84,7 +92,6 @@ entity User {=psl
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
referrer String @default("unknown")
|
||||
stripeId String?
|
||||
checkoutSessionId String?
|
||||
hasPaid Boolean @default(false)
|
||||
@ -95,6 +102,8 @@ entity User {=psl
|
||||
relatedObject RelatedObject[]
|
||||
externalAuthAssociations SocialLogin[]
|
||||
contactFormMessages ContactFormMessage[]
|
||||
referrer Referrer? @relation(fields: [referrerId], references: [id])
|
||||
referrerId Int?
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
@ -138,6 +147,20 @@ entity DailyStats {=psl
|
||||
totalProfit Int @default(0)
|
||||
psl=}
|
||||
|
||||
entity Referrer {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
name String @default("unknown") @unique
|
||||
count Int @default(0)
|
||||
users User[]
|
||||
psl=}
|
||||
|
||||
entity Logs {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
message String
|
||||
level String
|
||||
psl=}
|
||||
|
||||
/* 📡 These are the Wasp Routes (You can protect them easily w/ 'authRequired: true');
|
||||
* https://wasp-lang.dev/docs/tutorial/pages
|
||||
*/
|
||||
@ -174,24 +197,24 @@ page EmailVerificationPage {
|
||||
|
||||
route GptRoute { path: "/gpt", to: GptPage }
|
||||
page GptPage {
|
||||
component: import GptPage from "@client/GptPage"
|
||||
component: import GptPage from "@client/app/GptPage"
|
||||
}
|
||||
|
||||
route PricingRoute { path: "/pricing", to: PricingPage }
|
||||
page PricingPage {
|
||||
component: import Pricing from "@client/PricingPage"
|
||||
component: import Pricing from "@client/app/PricingPage"
|
||||
}
|
||||
|
||||
route AccountRoute { path: "/account", to: AccountPage }
|
||||
page AccountPage {
|
||||
authRequired: true,
|
||||
component: import Account from "@client/AccountPage"
|
||||
component: import Account from "@client/app/AccountPage"
|
||||
}
|
||||
|
||||
route CheckoutRoute { path: "/checkout", to: CheckoutPage }
|
||||
page CheckoutPage {
|
||||
authRequired: true,
|
||||
component: import Checkout from "@client/CheckoutPage"
|
||||
component: import Checkout from "@client/app/CheckoutPage"
|
||||
}
|
||||
|
||||
route AdminRoute { path: "/admin", to: DashboardPage }
|
||||
@ -269,11 +292,26 @@ action stripePayment {
|
||||
// entities: [User]
|
||||
// }
|
||||
|
||||
action updateUser {
|
||||
fn: import { updateUser } from "@server/actions.js",
|
||||
action updateCurrentUser {
|
||||
fn: import { updateCurrentUser } from "@server/actions.js",
|
||||
entities: [User]
|
||||
}
|
||||
|
||||
action updateUserById {
|
||||
fn: import { updateUserById } from "@server/actions.js",
|
||||
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 {
|
||||
@ -286,6 +324,16 @@ 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]
|
||||
}
|
||||
|
||||
/*
|
||||
* 📡 These are custom Wasp API Endpoints. Use them for callbacks, webhooks, etc.
|
||||
* https://wasp-lang.dev/docs/advanced/apis
|
||||
@ -318,7 +366,7 @@ job dailyStats {
|
||||
fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 * * * *" // every hour
|
||||
cron: "* * * * *" //
|
||||
},
|
||||
entities: [User, DailyStats]
|
||||
entities: [User, DailyStats, Logs]
|
||||
}
|
||||
|
9
migrations/20231115134355_logs/migration.sql
Normal file
@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Logs" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"message" TEXT NOT NULL,
|
||||
"level" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
24
migrations/20231115161051_referrer_object/migration.sql
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `referrer` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "referrer";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Referrer" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"referrer" TEXT NOT NULL DEFAULT 'unknown',
|
||||
"count" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Referrer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Referrer_userId_key" ON "Referrer"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Referrer" ADD CONSTRAINT "Referrer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[referrer]` on the table `Referrer` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Referrer_referrer_key" ON "Referrer"("referrer");
|
20
migrations/20231115161339_referrer_again/migration.sql
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `userId` on the `Referrer` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Referrer" DROP CONSTRAINT "Referrer_userId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Referrer_userId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Referrer" DROP COLUMN "userId";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "referrerId" INTEGER;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_referrerId_fkey" FOREIGN KEY ("referrerId") REFERENCES "Referrer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
16
migrations/20231115161543_referrer_name/migration.sql
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `referrer` on the `Referrer` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[name]` on the table `Referrer` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "Referrer_referrer_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Referrer" DROP COLUMN "referrer",
|
||||
ADD COLUMN "name" TEXT NOT NULL DEFAULT 'unknown';
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Referrer_name_key" ON "Referrer"("name");
|
@ -1,10 +1,12 @@
|
||||
import './Main.css';
|
||||
import NavBar from './NavBar';
|
||||
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 updateUser from '@wasp/actions/updateUser.js';
|
||||
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
|
||||
@ -16,7 +18,7 @@ export default function App({ children }: { children: ReactNode }) {
|
||||
const [referrer, setReferrer] = useReferrer();
|
||||
|
||||
const shouldDisplayAppNavBar = useMemo(() => {
|
||||
return location.pathname !== '/';
|
||||
return location.pathname !== '/' && location.pathname !== '/login' && location.pathname !== '/signup';
|
||||
}, [location]);
|
||||
|
||||
const isAdminDashboard = useMemo(() => {
|
||||
@ -28,14 +30,27 @@ export default function App({ children }: { children: ReactNode }) {
|
||||
const lastSeenAt = new Date(user.lastActiveTimestamp);
|
||||
const today = new Date();
|
||||
if (lastSeenAt.getDate() === today.getDate()) return;
|
||||
updateUser({ lastActiveTimestamp: today });
|
||||
updateCurrentUser({ lastActiveTimestamp: today });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && referrer && referrer !== UNKOWN_REFERRER) {
|
||||
updateUser({ referrer });
|
||||
setReferrer(null);
|
||||
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]);
|
||||
|
||||
@ -45,7 +60,7 @@ export default function App({ children }: { children: ReactNode }) {
|
||||
<>{children}</>
|
||||
) : (
|
||||
<>
|
||||
{shouldDisplayAppNavBar && <NavBar />}
|
||||
{shouldDisplayAppNavBar && <AppNavBar />}
|
||||
<div className='mx-auto max-w-7xl sm:px-6 lg:px-8 '>{children}</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,153 +0,0 @@
|
||||
export default function MainPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className='mx-auto max-w-7xl pt-10 pb-24 sm:pb-32 lg:grid lg:grid-cols-2 lg:gap-x-8 lg:py-32 lg:px-8'>
|
||||
<div className='px-6 lg:px-0 lg:pt-4'>
|
||||
<div className='mx-auto max-w-2xl'>
|
||||
<div className='max-w-lg'>
|
||||
<h1 className=' text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl'>SaaS Template</h1>
|
||||
|
||||
<h2 className='ml-4 max-w-2xl text-2xl f tracking-tight text-gray-800 slg:col-span-2 xl:col-auto'>
|
||||
for the PERN stack
|
||||
</h2>
|
||||
<h2 className='ml-4 max-w-2xl text-md f tracking-tight text-gray-600 slg:col-span-2 xl:col-auto'>
|
||||
Postgres/Prisma, Express, React, Node
|
||||
</h2>
|
||||
|
||||
<p className='mt-4 text-lg leading-8 text-gray-600'>
|
||||
Hey 🧙♂️! This template will help you get a SaaS App up and running in no time. It's got:
|
||||
</p>
|
||||
<ul className='list-disc ml-8 my-2 leading-8 text-gray-600'>
|
||||
<li>Stripe integration</li>
|
||||
<li>Authentication w/ Google</li>
|
||||
<li>OpenAI GPT API configuration</li>
|
||||
<li>Managed Server-Side Routes</li>
|
||||
<li>Tailwind styling</li>
|
||||
<li>Client-side Caching</li>
|
||||
<li>
|
||||
One-command{' '}
|
||||
<a href='https://wasp-lang.dev/docs/deploying' className='underline' target='_blank'>
|
||||
Deploy 🚀
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p className='mt-4 text-lg leading-8 text-gray-600'>
|
||||
Make sure to check out the <code>README.md</code> file and add your <code>env</code> variables before
|
||||
you begin
|
||||
</p>
|
||||
<div className='mt-10 flex items-center gap-x-6'>
|
||||
<span className='text-sm font-semibold leading-6 text-gray-900'>Made with Wasp {' = }'}</span>
|
||||
<a
|
||||
href='https://wasp-lang.dev/docs'
|
||||
className='rounded-md bg-yellow-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-yellow-400 hover:text-black/70 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500'
|
||||
>
|
||||
Read the Wasp Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-20 sm:mt-24 lg:mx-0 md:mx-auto md:max-w-2xl lg:w-screen lg:mt-0 '>
|
||||
<div className='shadow-lg md:rounded-3xl relative isolate overflow-hidden'>
|
||||
<div className='bg-yellow-500 [clip-path:inset(0)] md:[clip-path:inset(0_round_theme(borderRadius.3xl))]'>
|
||||
<div
|
||||
className='absolute -inset-y-px -z-10 ml-40 w-[200%] bg-yellow-100 opacity-20 ring-1 ring-inset ring-white '
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<div className='relative px-6 pt-8 sm:pt-16 md:pl-16 md:pr-0'>
|
||||
<div className='mx-auto max-w-2xl md:mx-0 md:max-w-none'>
|
||||
<div className='overflow-hidden rounded-tl-xl bg-gray-900'>
|
||||
<div className='bg-white/40 ring-1 ring-white/5'>
|
||||
<div className='-mb-px flex text-sm font-medium leading-6 text-gray-400'>
|
||||
<div className='border-b border-r border-b-white/20 border-r-white/10 bg-white/5 py-2 px-4 text-white'>
|
||||
main.wasp
|
||||
</div>
|
||||
<div className='border-r border-gray-600/10 py-2 px-4'>App.tsx</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 pt-6 pb-14 bg-gray-100'>
|
||||
<code className='language-javascript' style={{ whiteSpace: 'pre' }}>
|
||||
<span>{'app todoApp {'}</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>title</span>
|
||||
<span>: </span>
|
||||
<span style={{ color: '#50a14f' }}>"ToDo App"</span>
|
||||
<span>, </span>
|
||||
<span style={{ color: '#a0a1a7', fontStyle: 'italic' }}>/* visible in the browser tab */</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>auth</span>
|
||||
<span>{': {'} </span>
|
||||
<span style={{ color: '#a0a1a7', fontStyle: 'italic' }}>
|
||||
/* full-stack auth out-of-the-box */
|
||||
</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>userEntity</span>
|
||||
<span>: User,</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>externalAuthEntity</span>
|
||||
<span>: SocialLogin,</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>methods</span>
|
||||
<span>{': {'}</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>google</span>
|
||||
<span>{': {}'}</span>
|
||||
<br></br>
|
||||
{' }'}
|
||||
<br></br>
|
||||
{'}'}
|
||||
{/* */}
|
||||
<br />
|
||||
{/* */}
|
||||
<br />
|
||||
<span>{'route RootRoute { '}</span>
|
||||
<span style={{ color: '#986801' }}>path</span>
|
||||
<span>: </span>
|
||||
<span style={{ color: '#50a14f' }}>'/'</span>
|
||||
<span>, </span>
|
||||
<span style={{ color: '#986801' }}>to</span>
|
||||
<span>{': MainPage }'}</span>
|
||||
<br />
|
||||
{'page MainPage {'}
|
||||
<span> </span>
|
||||
<span style={{ color: '#a0a1a7', fontStyle: 'italic' }}>
|
||||
{'/* Only logged in users can access this. */'}
|
||||
</span>
|
||||
<span></span>
|
||||
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>authRequired</span>
|
||||
<span>: </span>
|
||||
<span style={{ color: '#0184bb' }}>true</span>
|
||||
<span>,</span>
|
||||
<br />
|
||||
<span>{' '}</span>
|
||||
<span style={{ color: '#986801' }}>component</span>
|
||||
<span>: </span>
|
||||
<span style={{ color: '#a626a4' }}>import</span>
|
||||
<span> Main </span>
|
||||
<span style={{ color: '#a626a4' }}>from</span>
|
||||
<span> </span>
|
||||
<span style={{ color: '#50a14f' }}>'@client/Main.jsx'</span>
|
||||
<br />
|
||||
<span></span>
|
||||
{'}'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
import { DailyStats } from "@wasp/entities";
|
||||
|
||||
export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?:DailyStats[], isLoading?: Boolean }
|
||||
export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?:DailyStats[], isLoading?: boolean }
|
@ -1,9 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Logo from '../images/logo/logo-icon.svg';
|
||||
import DarkModeSwitcher from './DarkModeSwitcher';
|
||||
import DropdownMessage from './DropdownMessage';
|
||||
import DropdownUser from '../../common/DropdownUser';
|
||||
import DropdownUser from '../../components/DropdownUser';
|
||||
import type { User } from '@wasp/entities'
|
||||
|
||||
const Header = (props: {
|
||||
@ -11,18 +8,11 @@ const Header = (props: {
|
||||
setSidebarOpen: (arg0: boolean) => void;
|
||||
user?: Omit<User, 'password'> | null | undefined;
|
||||
}) => {
|
||||
// const [username, setUsername] = useState<string | undefined>(undefined)
|
||||
// useEffect(() => {
|
||||
// if (props.user) {
|
||||
// setUsername(props.user?.email?.split('@')[0])
|
||||
// }
|
||||
// }, [props.user])
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-999 flex w-full bg-white dark:bg-boxdark dark:drop-shadow-none'>
|
||||
<div className='flex flex-grow items-center justify-between px-8 py-5 shadow '>
|
||||
<div className='flex flex-grow items-center justify-end px-8 py-5 shadow '>
|
||||
<div className='flex items-center gap-2 sm:gap-4 lg:hidden'>
|
||||
{/* <!-- Hamburger Toggle BTN --> */}
|
||||
{/* <!-- Hamburger Toggle BTN --> */} // TODO check mobile views
|
||||
<button
|
||||
aria-controls='sidebar'
|
||||
onClick={(e) => {
|
||||
@ -65,46 +55,10 @@ const Header = (props: {
|
||||
</button>
|
||||
{/* <!-- Hamburger Toggle BTN --> */}
|
||||
|
||||
<Link className='block flex-shrink-0 lg:hidden' to='/'>
|
||||
<img src={Logo} alt='Logo' />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='hidden sm:block'>
|
||||
<form action='https://formbold.com/s/unique_form_id' method='POST'>
|
||||
<div className='relative'>
|
||||
<button className='absolute top-1/2 left-0 -translate-y-1/2'>
|
||||
<svg
|
||||
className='fill-body hover:fill-primary dark:fill-bodydark dark:hover:fill-primary'
|
||||
width='20'
|
||||
height='20'
|
||||
viewBox='0 0 20 20'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M9.16666 3.33332C5.945 3.33332 3.33332 5.945 3.33332 9.16666C3.33332 12.3883 5.945 15 9.16666 15C12.3883 15 15 12.3883 15 9.16666C15 5.945 12.3883 3.33332 9.16666 3.33332ZM1.66666 9.16666C1.66666 5.02452 5.02452 1.66666 9.16666 1.66666C13.3088 1.66666 16.6667 5.02452 16.6667 9.16666C16.6667 13.3088 13.3088 16.6667 9.16666 16.6667C5.02452 16.6667 1.66666 13.3088 1.66666 9.16666Z'
|
||||
fill=''
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M13.2857 13.2857C13.6112 12.9603 14.1388 12.9603 14.4642 13.2857L18.0892 16.9107C18.4147 17.2362 18.4147 17.7638 18.0892 18.0892C17.7638 18.4147 17.2362 18.4147 16.9107 18.0892L13.2857 14.4642C12.9603 14.1388 12.9603 13.6112 13.2857 13.2857Z'
|
||||
fill=''
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Type to search...'
|
||||
className='w-full bg-transparent pr-4 pl-9 focus:outline-none'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/* <div className='hidden sm:block'>
|
||||
</div> */}
|
||||
|
||||
<div className='flex items-center gap-3 2xsm:gap-7'>
|
||||
<ul className='flex items-center gap-2 2xsm:gap-4'>
|
||||
@ -118,7 +72,7 @@ const Header = (props: {
|
||||
</ul>
|
||||
|
||||
{/* <!-- User Area --> */}
|
||||
<DropdownUser username={props.user?.email?.split('@')[0]} />
|
||||
<DropdownUser username={props.user?.email?.split('@')[0]} isUserAdmin={props.user?.isAdmin || false} />
|
||||
{/* <!-- User Area --> */}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,173 +1,54 @@
|
||||
import BrandOne from '../images/brand/brand-01.svg';
|
||||
import BrandTwo from '../images/brand/brand-02.svg';
|
||||
import BrandThree from '../images/brand/brand-03.svg';
|
||||
import BrandFour from '../images/brand/brand-04.svg';
|
||||
import BrandFive from '../images/brand/brand-05.svg';
|
||||
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 Channels
|
||||
</h4>
|
||||
<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-5">
|
||||
<div className="p-2.5 xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
Source
|
||||
</h5>
|
||||
<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 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">
|
||||
Revenues
|
||||
</h5>
|
||||
<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 className="hidden p-2.5 text-center sm:block xl:p-5">
|
||||
<h5 className="text-sm font-medium uppercase xsm:text-base">
|
||||
Conversion
|
||||
</h5>
|
||||
<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>
|
||||
|
||||
<div className="grid grid-cols-3 border-b border-stroke dark:border-strokedark sm:grid-cols-5">
|
||||
<div className="flex items-center gap-3 p-2.5 xl:p-5">
|
||||
<div className="flex-shrink-0">
|
||||
<img src={BrandOne} alt="Brand" />
|
||||
{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>
|
||||
<p className="hidden text-black dark:text-white sm:block">Google</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-black dark:text-white">3.5K</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-meta-3">$5,768</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-black dark:text-white">590</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-meta-5">4.8%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 border-b border-stroke dark:border-strokedark sm:grid-cols-5">
|
||||
<div className="flex items-center gap-3 p-2.5 xl:p-5">
|
||||
<div className="flex-shrink-0">
|
||||
<img src={BrandTwo} alt="Brand" />
|
||||
</div>
|
||||
<p className="hidden text-black dark:text-white sm:block">
|
||||
Twitter
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-black dark:text-white">2.2K</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-meta-3">$4,635</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-black dark:text-white">467</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-meta-5">4.3%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 border-b border-stroke dark:border-strokedark sm:grid-cols-5">
|
||||
<div className="flex items-center gap-3 p-2.5 xl:p-5">
|
||||
<div className="flex-shrink-0">
|
||||
<img src={BrandThree} alt="Brand" />
|
||||
</div>
|
||||
<p className="hidden text-black dark:text-white sm:block">Github</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-black dark:text-white">2.1K</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-meta-3">$4,290</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-black dark:text-white">420</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-meta-5">3.7%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 border-b border-stroke dark:border-strokedark sm:grid-cols-5">
|
||||
<div className="flex items-center gap-3 p-2.5 xl:p-5">
|
||||
<div className="flex-shrink-0">
|
||||
<img src={BrandFour} alt="Brand" />
|
||||
</div>
|
||||
<p className="hidden text-black dark:text-white sm:block">Vimeo</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-black dark:text-white">1.5K</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-meta-3">$3,580</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-black dark:text-white">389</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-meta-5">2.5%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 sm:grid-cols-5">
|
||||
<div className="flex items-center gap-3 p-2.5 xl:p-5">
|
||||
<div className="flex-shrink-0">
|
||||
<img src={BrandFive} alt="Brand" />
|
||||
</div>
|
||||
<p className="hidden text-black dark:text-white sm:block">
|
||||
Facebook
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-black dark:text-white">1.2K</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-2.5 xl:p-5">
|
||||
<p className="text-meta-3">$2,740</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-black dark:text-white">230</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center justify-center p-2.5 sm:flex xl:p-5">
|
||||
<p className="text-meta-5">1.9%</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -58,7 +58,7 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
return (
|
||||
<aside
|
||||
ref={sidebar}
|
||||
className={`absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-black duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
||||
className={`absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-gray-800 duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
@ -105,9 +105,9 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
{/* <!-- Menu Item Dashboard --> */}
|
||||
<NavLink
|
||||
to="/admin"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' &&
|
||||
'bg-graydark dark:bg-meta-4' )
|
||||
'bg-gray-700 dark:bg-meta-4' )
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
@ -144,8 +144,8 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<li>
|
||||
<NavLink
|
||||
to="/admin/users"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
pathname.includes('users') && 'bg-graydark dark:bg-meta-4'
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
pathname.includes('users') && 'bg-gray-700 dark:bg-meta-4'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
@ -182,9 +182,9 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<li>
|
||||
<NavLink
|
||||
to="/admin/settings"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
pathname.includes('settings') &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
'bg-gray-700 dark:bg-meta-4'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
@ -234,8 +234,8 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<li>
|
||||
<NavLink
|
||||
to="/admin/chart"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
pathname.includes('chart') && 'bg-graydark dark:bg-meta-4'
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
pathname.includes('chart') && 'bg-gray-700 dark:bg-meta-4'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
@ -283,10 +283,10 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<React.Fragment>
|
||||
<NavLink
|
||||
to="#"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
(pathname === '/forms' ||
|
||||
pathname.includes('forms')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
'bg-gray-700 dark:bg-meta-4'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@ -385,9 +385,9 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<li>
|
||||
<NavLink
|
||||
to="/admin/calendar"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
pathname.includes('calendar') &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
'bg-gray-700 dark:bg-meta-4'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
@ -417,9 +417,9 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
|
||||
<React.Fragment>
|
||||
<NavLink
|
||||
to="#"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm py-2 px-4 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-gray-700 dark:hover:bg-meta-4 ${
|
||||
(pathname === '/ui' || pathname.includes('ui')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
'bg-gray-700 dark:bg-meta-4'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -1,24 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { User } from '@wasp/entities';
|
||||
|
||||
const SwitcherOne = () => {
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
const SwitcherOne = ({ user, updateUserById}: { user?: Partial<User>, updateUserById?: any}) => {
|
||||
const [enabled, setEnabled] = useState<boolean>(user?.hasPaid || false);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.table({ hasPaid: user?.hasPaid})
|
||||
// }, [user])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="toggle1"
|
||||
className="flex cursor-pointer select-none items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className='relative'>
|
||||
<label htmlFor={`toggle1-${user?.id}`} className='flex cursor-pointer select-none items-center'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle1"
|
||||
className="sr-only"
|
||||
type='checkbox'
|
||||
id={`toggle1-${user?.id}`}
|
||||
className='sr-only'
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
updateUserById && updateUserById({ id: user?.id, data: { hasPaid: !enabled } });
|
||||
}}
|
||||
/>
|
||||
<div className="block h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]"></div>
|
||||
<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 ${
|
||||
enabled && '!right-1 !translate-x-full !bg-primary dark:!bg-white'
|
||||
|
@ -1,3 +1,5 @@
|
||||
// TODO hook up to Google Analytics or Plausible
|
||||
|
||||
const TotalPageViewsCard = () => {
|
||||
return (
|
||||
<div className="rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { UpArrow, DownArrow } from '../common/icons';
|
||||
import { UpArrow, DownArrow } from '../images/icon/icons-arrows';
|
||||
import type { DailyStatsProps } from '../common/types';
|
||||
|
||||
const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { UpArrow, DownArrow } from '../common/icons';
|
||||
import { UpArrow, DownArrow } from '../images/icon/icons-arrows';
|
||||
import type { DailyStatsProps } from '../common/types';
|
||||
|
||||
const TotalRevenueCard = ({dailyStats, weeklyStats, isLoading}: DailyStatsProps) => {
|
||||
@ -8,16 +8,18 @@ const TotalRevenueCard = ({dailyStats, weeklyStats, isLoading}: DailyStatsProps)
|
||||
return (weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) > 0;
|
||||
}, [weeklyStats]);
|
||||
|
||||
const delta = useMemo(() => {
|
||||
if (!weeklyStats) return;
|
||||
return weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue;
|
||||
}, [weeklyStats]);
|
||||
|
||||
const deltaPercentage = useMemo(() => {
|
||||
if (!weeklyStats || !weeklyStats[1]?.totalRevenue) return;
|
||||
if ( !weeklyStats || isLoading) return;
|
||||
weeklyStats.sort((a, b) => b.id - a.id);
|
||||
console.log('weeklyStats[1]?.totalRevenue; ', !!weeklyStats && weeklyStats)
|
||||
return ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
|
||||
}, [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'>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { UpArrow } from '../common/icons';
|
||||
import { UpArrow } from '../images/icon/icons-arrows';
|
||||
import type { DailyStatsProps } from '../common/types';
|
||||
|
||||
const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
|
||||
|
@ -1,59 +1,195 @@
|
||||
import SwitcherOne from "./SwitcherOne";
|
||||
import DropdownEditDelete from "./DropdownEditDelete";
|
||||
import { useState, useEffect } from 'react';
|
||||
import SwitcherOne from './SwitcherOne';
|
||||
import DropdownEditDelete from './DropdownEditDelete';
|
||||
import { useQuery } from '@wasp/queries';
|
||||
import getPaginatedUsers from '@wasp/queries/getPaginatedUsers';
|
||||
import updateUserById from '@wasp/actions/updateUserById';
|
||||
import Loader from '../common/Loader';
|
||||
|
||||
// TODO extract hasPaid to its own value
|
||||
type StatusOptions = 'hasPaid' | 'past_due' | 'canceled' | 'active';
|
||||
|
||||
const UsersTable = () => {
|
||||
const [skip, setskip] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [email, setEmail] = useState<string | undefined>(undefined);
|
||||
const [statusOptions, setStatusOptions] = useState<StatusOptions[]>([]);
|
||||
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
|
||||
skip,
|
||||
emailContains: email,
|
||||
subscriptionStatus: statusOptions?.length > 0 ? statusOptions : undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [email, statusOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
setskip((page - 1) * 10);
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="py-6 px-4 md:px-6 xl:px-7.5">
|
||||
<h4 className="text-xl font-semibold text-black dark:text-white">
|
||||
Users
|
||||
</h4>
|
||||
</div>
|
||||
<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 items-center justify-between gap-3 w-full py-6 px-4 md:px-6 xl:px-7.5'>
|
||||
<div className='relative flex items-center gap-3 p-4'>
|
||||
<span>Filters:</span>
|
||||
{/* <label className='block text-black dark:text-white whitespace-nowrap'>Search by Email</label> */}
|
||||
<input
|
||||
type='text'
|
||||
placeholder='dude@example.com'
|
||||
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'
|
||||
/>
|
||||
{/* <label className='mb-3 block text-black dark:text-white whitespace-nowrap'>Multiselect Dropdown</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 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'
|
||||
>
|
||||
{opt}
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setStatusOptions((prevValue) => {
|
||||
return prevValue?.filter((val) => val !== opt);
|
||||
});
|
||||
}}
|
||||
className='z-30 cursor-pointer pl-2 hover:text-danger'
|
||||
>
|
||||
<svg width='14' height='14' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z'
|
||||
fill='currentColor'
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</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'>
|
||||
Select Payment Status Filters
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<select
|
||||
onChange={(e) => {
|
||||
setStatusOptions((prevValue) => {
|
||||
if (prevValue?.includes(e.target.value as StatusOptions)) {
|
||||
return prevValue?.filter((val) => val !== e.target.value);
|
||||
} else if (!!prevValue) {
|
||||
return [...prevValue, e.target.value as StatusOptions];
|
||||
} else {
|
||||
return [prevValue as StatusOptions];
|
||||
}
|
||||
});
|
||||
}}
|
||||
name='status'
|
||||
id='status'
|
||||
className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'
|
||||
>
|
||||
<option value=''>Select filters</option>
|
||||
{['hasPaid', 'past_due', 'canceled', 'active'].map((status) => {
|
||||
if (!statusOptions.includes(status as StatusOptions)) {
|
||||
return <option value={status}>{status}</option>;
|
||||
}
|
||||
})}
|
||||
</select>
|
||||
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
|
||||
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g opacity='0.8'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
|
||||
fill='#637381'
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-6 border-t border-stroke py-4.5 px-4 dark:border-strokedark sm:grid-cols-8 md:px-6 2xl:px-7.5">
|
||||
<div className="col-span-2 flex items-center">
|
||||
<p className="font-medium">Email</p>
|
||||
</div>
|
||||
<div className="col-span-1 hidden items-center sm:flex">
|
||||
<p className="font-medium">Last Active</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<p className="font-medium">Has Paid</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<p className="font-medium">Subscription Status</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<p className="font-medium">Total Paid</p>
|
||||
{!isLoading && (
|
||||
<div className='max-w-60'>
|
||||
<span className='text-md mr-2 text-black dark:text-white'>page</span>
|
||||
<input
|
||||
type='number'
|
||||
value={page}
|
||||
min={1}
|
||||
max={data?.totalPages}
|
||||
onChange={(e) => {
|
||||
setPage(parseInt(e.currentTarget.value));
|
||||
}}
|
||||
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'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-6 border-t border-stroke py-4.5 px-4 dark:border-strokedark sm:grid-cols-8 md:px-6 2xl:px-7.5">
|
||||
<div className="col-span-2 flex items-center">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<p className="text-sm text-black dark:text-white">
|
||||
Apple Watch Series 7
|
||||
</p>
|
||||
<div className='grid grid-cols-12 border-t border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '>
|
||||
<div className='col-span-3 flex items-center'>
|
||||
<p className='font-medium'>Email</p>
|
||||
</div>
|
||||
<div className='col-span-3 hidden items-center sm:flex'>
|
||||
<p className='font-medium'>Last Active</p>
|
||||
</div>
|
||||
<div className='col-span-2 flex items-center'>
|
||||
<p className='font-medium'>Subscription Status</p>
|
||||
</div>
|
||||
<div className='col-span-2 flex items-center'>
|
||||
<p className='font-medium'>Stripe ID</p>
|
||||
</div>
|
||||
<div className='col-span-1 flex items-center'>
|
||||
<p className='font-medium'>Has Paid</p>
|
||||
</div>
|
||||
<div className='col-span-1 flex items-center'>
|
||||
<p className='font-medium'>Delete User</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 hidden items-center sm:flex">
|
||||
<p className="text-sm text-black dark:text-white">Electronics</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<p className="text-sm text-black dark:text-white">
|
||||
<SwitcherOne />
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<p className="text-sm text-black dark:text-white">22</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<p className="text-sm text-meta-3">$45</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<DropdownEditDelete />
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className='-mt-40'>
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{!!data?.users &&
|
||||
data?.users?.length > 0 &&
|
||||
data.users.map((user) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className='grid grid-cols-12 gap-4 border-t border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '
|
||||
>
|
||||
<div className='col-span-3 flex items-center'>
|
||||
<div className='flex flex-col gap-4 sm:flex-row sm:items-center'>
|
||||
<p className='text-sm text-black dark:text-white'>{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-span-3 hidden items-center sm:flex'>
|
||||
<p className='text-sm text-black dark:text-white'>{user.lastActiveTimestamp.toISOString()}</p>
|
||||
</div>
|
||||
<div className='col-span-2 flex items-center'>
|
||||
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
|
||||
</div>
|
||||
<div className='col-span-2 flex items-center'>
|
||||
<p className='text-sm text-meta-3'>{user.stripeId}</p>
|
||||
</div>
|
||||
<div className='col-span-1 flex items-center'>
|
||||
<div className='text-sm text-black dark:text-white'>
|
||||
<SwitcherOne user={user} updateUserById={updateUserById} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-span-1 flex items-center'>
|
||||
<DropdownEditDelete />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,14 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="24" fill="#EEF0F2"/>
|
||||
<g clip-path="url(#clip0_38_23844)">
|
||||
<path d="M36.0002 24.2661C36.0147 23.4411 35.9278 22.6174 35.7414 21.8128H24.2451V26.2661H30.9933C30.8655 27.0469 30.5778 27.7943 30.1476 28.4634C29.7174 29.1324 29.1536 29.7093 28.49 30.1593L28.4665 30.3085L32.1016 33.0681L32.3533 33.0928C34.6661 30.9994 35.9997 27.9193 35.9997 24.2661" fill="#4285F4"/>
|
||||
<path d="M24.2449 36C27.5509 36 30.3264 34.9332 32.3539 33.0932L28.4898 30.1597C27.4559 30.8666 26.0681 31.36 24.2449 31.36C22.6965 31.3511 21.1902 30.8646 19.9398 29.9695C18.6894 29.0744 17.7584 27.8161 17.2789 26.3732L17.1354 26.3852L13.3556 29.2518L13.3062 29.3865C14.3241 31.3747 15.8864 33.0463 17.8183 34.2142C19.7502 35.3822 21.9755 36.0005 24.2454 36" fill="#34A853"/>
|
||||
<path d="M17.2788 26.3733C17.011 25.6094 16.8728 24.8077 16.8697 24C16.8746 23.1936 17.0077 22.393 17.2642 21.6267L17.2574 21.4677L13.4312 18.5549L13.3061 18.6133C12.4473 20.2842 12 22.129 12 23.9999C12 25.8708 12.4473 27.7156 13.3061 29.3865L17.2788 26.3733Z" fill="#FBBC05"/>
|
||||
<path d="M24.2449 16.64C25.9995 16.6133 27.6964 17.2537 28.9796 18.4267L32.4355 15.12C30.219 13.0822 27.2838 11.9642 24.2449 12C21.975 11.9995 19.7497 12.6177 17.8179 13.7856C15.886 14.9535 14.3237 16.625 13.3057 18.6132L17.2652 21.6267C17.7495 20.1841 18.6836 18.9268 19.9358 18.0321C21.1881 17.1374 22.6953 16.6505 24.2449 16.64Z" fill="#EB4335"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_38_23844">
|
||||
<rect width="24" height="24" fill="white" transform="translate(12 12)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24.0001" r="24" fill="#2B96F0"/>
|
||||
<path d="M33.0375 17.7376L34.4625 15.9376C34.875 15.4501 34.9875 15.0751 35.025 14.8876C33.9 15.5626 32.85 15.7876 32.175 15.7876H31.9125L31.7625 15.6376C30.8625 14.8501 29.7375 14.4376 28.5375 14.4376C25.9125 14.4376 23.85 16.6126 23.85 19.1251C23.85 19.2751 23.85 19.5001 23.8875 19.6501L24 20.4001L23.2125 20.3626C18.4125 20.2126 14.475 16.0876 13.8375 15.3751C12.7875 17.2501 13.3875 19.0501 14.025 20.1751L15.3 22.2751L13.275 21.1501C13.3125 22.7251 13.9125 23.9626 15.075 24.8626L16.0875 25.6126L15.075 26.0251C15.7125 27.9376 17.1375 28.7251 18.1875 29.0251L19.575 29.4001L18.2625 30.3001C16.1625 31.8001 13.5375 31.6876 12.375 31.5751C14.7375 33.2251 17.55 33.6001 19.5 33.6001C20.9625 33.6001 22.05 33.4501 22.3125 33.3376C32.8125 30.8626 33.3 21.4876 33.3 19.6126V19.3501L33.525 19.2001C34.8 18.0001 35.325 17.3626 35.625 16.9876C35.5125 17.0251 35.3625 17.1001 35.2125 17.1376L33.0375 17.7376Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,11 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24.0001" r="24" fill="#212529"/>
|
||||
<g clip-path="url(#clip0_38_23867)">
|
||||
<path d="M24 12.6751C17.625 12.6751 12.375 17.8501 12.375 24.3001C12.375 29.4001 15.7125 33.7501 20.3625 35.3251C20.9625 35.4376 21.15 35.0626 21.15 34.8001C21.15 34.5376 21.15 33.7876 21.1125 32.7751C17.8875 33.5251 17.2125 31.2001 17.2125 31.2001C16.6875 29.8876 15.9 29.5126 15.9 29.5126C14.85 28.7626 15.9375 28.7626 15.9375 28.7626C17.1 28.8001 17.7375 29.9626 17.7375 29.9626C18.75 31.7626 20.475 31.2376 21.1125 30.9001C21.225 30.1501 21.525 29.6251 21.8625 29.3251C19.3125 29.0626 16.575 28.0501 16.575 23.6251C16.575 22.3501 17.0625 21.3376 17.775 20.5501C17.6625 20.2876 17.25 19.0876 17.8875 17.4751C17.8875 17.4751 18.9 17.1751 21.1125 18.6751C22.05 18.4126 23.025 18.2626 24.0375 18.2626C25.05 18.2626 26.0625 18.3751 26.9625 18.6751C29.175 17.2126 30.15 17.4751 30.15 17.4751C30.7875 19.0501 30.4125 20.2876 30.2625 20.5501C31.0125 21.3376 31.4625 22.3876 31.4625 23.6251C31.4625 28.0501 28.725 29.0626 26.175 29.3251C26.5875 29.7001 26.9625 30.4501 26.9625 31.5001C26.9625 33.0751 26.925 34.3126 26.925 34.6876C26.925 34.9876 27.15 35.3251 27.7125 35.2126C32.2875 33.6751 35.625 29.3626 35.625 24.2251C35.5875 17.8501 30.375 12.6751 24 12.6751Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_38_23867">
|
||||
<rect width="24" height="24" fill="white" transform="translate(12 12.0001)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24.0002" r="24" fill="#4DC1FF"/>
|
||||
<path d="M35.625 18.6002C35.5125 20.8877 33.9375 24.0002 30.8625 27.9377C27.7125 32.0627 25.0125 34.1252 22.8375 34.1252C21.525 34.1252 20.3625 32.9252 19.3875 30.3752C18.75 28.1252 18.1875 25.8002 17.5125 23.4752C16.8375 21.0002 16.0875 19.7252 15.225 19.7252C15.075 19.7252 14.4375 20.1002 13.3875 20.8502L12.375 19.3502C13.5375 18.3377 14.6625 17.3252 15.825 16.2752C17.3625 14.9627 18.45 14.2502 19.275 14.2127C21.075 14.0627 22.1625 15.3377 22.6125 17.9252C23.025 20.7752 23.3625 22.5752 23.55 23.2502C24.075 25.6127 24.675 26.7752 25.2375 26.7752C25.725 26.7752 26.4375 26.0252 27.45 24.4877C28.4625 22.9502 28.9125 21.7502 29.025 20.9627C29.175 19.6502 28.6125 18.9377 27.45 18.9377C26.925 18.9377 26.325 19.0502 25.7625 19.3502C26.8875 15.6002 29.025 13.7627 32.25 13.8377C34.6125 13.9502 35.7375 15.5252 35.625 18.6002Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1001 B |
@ -1,11 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24.0002" r="24" fill="#3162C9"/>
|
||||
<g clip-path="url(#clip0_38_23887)">
|
||||
<path d="M25.789 35.9983V25.0533H29.3269L29.8566 20.7878H25.789V18.0645C25.789 16.8295 26.1192 15.9879 27.8248 15.9879L30 15.9868V12.1719C29.6236 12.1201 28.3325 12.004 26.8304 12.004C23.6942 12.004 21.5471 13.9917 21.5471 17.6422V20.7879H18V25.0534H21.547V35.9984L25.789 35.9983Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_38_23887">
|
||||
<rect width="12" height="24" fill="white" transform="translate(18 12.0002)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 402 KiB |
Before Width: | Height: | Size: 457 KiB |
Before Width: | Height: | Size: 392 KiB |
Before Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 402 KiB |
Before Width: | Height: | Size: 427 KiB |
@ -1,33 +0,0 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_22_27)">
|
||||
<path d="M18.4739 0.411499H1.55556C0.696446 0.411499 0 1.10794 0 1.96705V12.1893C0 13.0484 0.696446 13.7449 1.55556 13.7449H18.4739C19.333 13.7449 20.0295 13.0484 20.0295 12.1893V1.96705C20.0295 1.10794 19.333 0.411499 18.4739 0.411499Z" fill="white"/>
|
||||
<mask id="mask0_22_27" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="21" height="14">
|
||||
<path d="M18.4739 0.411499H1.55556C0.696446 0.411499 0 1.10794 0 1.96705V12.1893C0 13.0484 0.696446 13.7449 1.55556 13.7449H18.4739C19.333 13.7449 20.0295 13.0484 20.0295 12.1893V1.96705C20.0295 1.10794 19.333 0.411499 18.4739 0.411499Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_22_27)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0295 0.411499H0V1.30039H20.0295V0.411499ZM20.0295 2.18928H0V3.07817H20.0295V2.18928ZM0 3.96705H20.0295V4.85594H0V3.96705ZM20.0295 5.74484H0V6.63372H20.0295V5.74484ZM0 7.52262H20.0295V8.41152H0V7.52262ZM20.0295 9.30037H0V10.1893H20.0295V9.30037ZM0 11.0781H20.0295V11.9671H0V11.0781ZM20.0295 12.856H0V13.7448H20.0295V12.856Z" fill="#D02F44"/>
|
||||
<path d="M8.5841 0.411499H0V6.63372H8.5841V0.411499Z" fill="#46467F"/>
|
||||
<g filter="url(#filter0_d_22_27)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.9074 1.74479C1.9074 1.99026 1.69389 2.18924 1.43051 2.18924C1.16713 2.18924 0.953613 1.99026 0.953613 1.74479C0.953613 1.49933 1.16713 1.30035 1.43051 1.30035C1.69389 1.30035 1.9074 1.49933 1.9074 1.74479ZM3.81497 1.74479C3.81497 1.99026 3.60146 2.18924 3.33809 2.18924C3.0747 2.18924 2.86119 1.99026 2.86119 1.74479C2.86119 1.49933 3.0747 1.30035 3.33809 1.30035C3.60146 1.30035 3.81497 1.49933 3.81497 1.74479ZM5.24566 2.18924C5.50903 2.18924 5.72255 1.99026 5.72255 1.74479C5.72255 1.49933 5.50903 1.30035 5.24566 1.30035C4.98228 1.30035 4.76876 1.49933 4.76876 1.74479C4.76876 1.99026 4.98228 2.18924 5.24566 2.18924ZM7.63012 1.74479C7.63012 1.99026 7.41661 2.18924 7.15322 2.18924C6.88985 2.18924 6.67634 1.99026 6.67634 1.74479C6.67634 1.49933 6.88985 1.30035 7.15322 1.30035C7.41661 1.30035 7.63012 1.49933 7.63012 1.74479ZM2.3843 3.07812C2.64768 3.07812 2.86119 2.87914 2.86119 2.63368C2.86119 2.38822 2.64768 2.18924 2.3843 2.18924C2.12092 2.18924 1.9074 2.38822 1.9074 2.63368C1.9074 2.87914 2.12092 3.07812 2.3843 3.07812ZM4.76876 2.63368C4.76876 2.87914 4.55525 3.07812 4.29187 3.07812C4.02849 3.07812 3.81497 2.87914 3.81497 2.63368C3.81497 2.38822 4.02849 2.18924 4.29187 2.18924C4.55525 2.18924 4.76876 2.38822 4.76876 2.63368ZM6.19944 3.07812C6.46282 3.07812 6.67634 2.87914 6.67634 2.63368C6.67634 2.38822 6.46282 2.18924 6.19944 2.18924C5.93606 2.18924 5.72255 2.38822 5.72255 2.63368C5.72255 2.87914 5.93606 3.07812 6.19944 3.07812ZM7.63012 3.52257C7.63012 3.76804 7.41661 3.96702 7.15322 3.96702C6.88985 3.96702 6.67634 3.76804 6.67634 3.52257C6.67634 3.27711 6.88985 3.07814 7.15322 3.07814C7.41661 3.07814 7.63012 3.27711 7.63012 3.52257ZM5.24566 3.96702C5.50903 3.96702 5.72255 3.76804 5.72255 3.52257C5.72255 3.27711 5.50903 3.07814 5.24566 3.07814C4.98228 3.07814 4.76876 3.27711 4.76876 3.52257C4.76876 3.76804 4.98228 3.96702 5.24566 3.96702ZM3.81497 3.52257C3.81497 3.76804 3.60146 3.96702 3.33809 3.96702C3.0747 3.96702 2.86119 3.76804 2.86119 3.52257C2.86119 3.27711 3.0747 3.07814 3.33809 3.07814C3.60146 3.07814 3.81497 3.27711 3.81497 3.52257ZM1.43051 3.96702C1.69389 3.96702 1.9074 3.76804 1.9074 3.52257C1.9074 3.27711 1.69389 3.07814 1.43051 3.07814C1.16713 3.07814 0.953613 3.27711 0.953613 3.52257C0.953613 3.76804 1.16713 3.96702 1.43051 3.96702ZM2.86119 4.41146C2.86119 4.65692 2.64768 4.8559 2.3843 4.8559C2.12092 4.8559 1.9074 4.65692 1.9074 4.41146C1.9074 4.166 2.12092 3.96702 2.3843 3.96702C2.64768 3.96702 2.86119 4.166 2.86119 4.41146ZM4.29187 4.8559C4.55525 4.8559 4.76876 4.65692 4.76876 4.41146C4.76876 4.166 4.55525 3.96702 4.29187 3.96702C4.02849 3.96702 3.81497 4.166 3.81497 4.41146C3.81497 4.65692 4.02849 4.8559 4.29187 4.8559ZM6.67634 4.41146C6.67634 4.65692 6.46282 4.8559 6.19944 4.8559C5.93606 4.8559 5.72255 4.65692 5.72255 4.41146C5.72255 4.166 5.93606 3.96702 6.19944 3.96702C6.46282 3.96702 6.67634 4.166 6.67634 4.41146ZM7.15322 5.74479C7.41661 5.74479 7.63012 5.54581 7.63012 5.30035C7.63012 5.05489 7.41661 4.8559 7.15322 4.8559C6.88985 4.8559 6.67634 5.05489 6.67634 5.30035C6.67634 5.54581 6.88985 5.74479 7.15322 5.74479ZM5.72255 5.30035C5.72255 5.54581 5.50903 5.74479 5.24566 5.74479C4.98228 5.74479 4.76876 5.54581 4.76876 5.30035C4.76876 5.05489 4.98228 4.8559 5.24566 4.8559C5.50903 4.8559 5.72255 5.05489 5.72255 5.30035ZM3.33809 5.74479C3.60146 5.74479 3.81497 5.54581 3.81497 5.30035C3.81497 5.05489 3.60146 4.8559 3.33809 4.8559C3.0747 4.8559 2.86119 5.05489 2.86119 5.30035C2.86119 5.54581 3.0747 5.74479 3.33809 5.74479ZM1.9074 5.30035C1.9074 5.54581 1.69389 5.74479 1.43051 5.74479C1.16713 5.74479 0.953613 5.54581 0.953613 5.30035C0.953613 5.05489 1.16713 4.8559 1.43051 4.8559C1.69389 4.8559 1.9074 5.05489 1.9074 5.30035Z" fill="url(#paint0_linear_22_27)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_22_27" x="-3.04639" y="-1.69965" width="14.6765" height="12.4445" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_22_27"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_22_27" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_22_27" x1="0.953613" y1="1.30035" x2="0.953613" y2="5.74479" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F0F0F0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_22_27">
|
||||
<rect width="20.2222" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.9 KiB |
@ -1,18 +0,0 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_22_42)">
|
||||
<path d="M18.4773 0.605957H1.55545C0.803726 0.605957 0.194336 1.21535 0.194336 1.96707V12.1893C0.194336 12.941 0.803726 13.5504 1.55545 13.5504H18.4773C19.229 13.5504 19.8384 12.941 19.8384 12.1893V1.96707C19.8384 1.21535 19.229 0.605957 18.4773 0.605957Z" fill="white" stroke="#F3F4F6" stroke-width="0.5"/>
|
||||
<mask id="mask0_22_42" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-1" y="0" width="22" height="14">
|
||||
<path d="M18.4773 0.605957H1.55545C0.803726 0.605957 0.194336 1.21535 0.194336 1.96707V12.1893C0.194336 12.941 0.803726 13.5504 1.55545 13.5504H18.4773C19.229 13.5504 19.8384 12.941 19.8384 12.1893V1.96707C19.8384 1.21535 19.229 0.605957 18.4773 0.605957Z" fill="white" stroke="white" stroke-width="0.5"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_22_42)">
|
||||
<path d="M20.0328 0.411499H14.3091V13.7449H20.0328V0.411499Z" fill="#FF3131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.7448H5.72372V0.411499H0V13.7448Z" fill="#FF3131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5736 6.51591C11.388 6.68884 11.0901 6.51985 11.1433 6.27182L11.4473 4.85581L10.4933 5.30026L10.0163 3.96692L9.53936 5.30026L8.58542 4.85581L8.8893 6.27182C8.94257 6.51985 8.64468 6.68884 8.45911 6.51591L8.2852 6.35383C8.18556 6.26105 8.03125 6.26105 7.9317 6.35383L7.63144 6.63358L6.67749 6.18915L7.15446 7.07803L6.88105 7.3328C6.77103 7.43532 6.77103 7.60963 6.88105 7.71215L8.10841 8.85584H9.53936L9.77783 10.1891H10.2548L10.4933 8.85584H11.9243L13.1516 7.71215C13.2617 7.60963 13.2617 7.43532 13.1516 7.3328L12.8782 7.07803L13.3551 6.18915L12.4012 6.63358L12.101 6.35383C12.0014 6.26105 11.847 6.26105 11.7475 6.35383L11.5736 6.51591Z" fill="#FF3131"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_22_42">
|
||||
<rect width="20.2222" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB |
@ -1,17 +0,0 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_22_57)">
|
||||
<path d="M18.4773 0.605957H1.55545C0.803726 0.605957 0.194336 1.21535 0.194336 1.96707V12.1893C0.194336 12.941 0.803726 13.5504 1.55545 13.5504H18.4773C19.229 13.5504 19.8384 12.941 19.8384 12.1893V1.96707C19.8384 1.21535 19.229 0.605957 18.4773 0.605957Z" fill="white" stroke="#F3F4F6" stroke-width="0.5"/>
|
||||
<mask id="mask0_22_57" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-1" y="0" width="22" height="14">
|
||||
<path d="M18.4773 0.605957H1.55545C0.803726 0.605957 0.194336 1.21535 0.194336 1.96707V12.1893C0.194336 12.941 0.803726 13.5504 1.55545 13.5504H18.4773C19.229 13.5504 19.8384 12.941 19.8384 12.1893V1.96707C19.8384 1.21535 19.229 0.605957 18.4773 0.605957Z" fill="white" stroke="white" stroke-width="0.5"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_22_57)">
|
||||
<path d="M20.0331 0.411499H13.3555V13.7449H20.0331V0.411499Z" fill="#F44653"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.7448H6.67767V0.411499H0V13.7448Z" fill="#1035BB"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_22_57">
|
||||
<rect width="20.2222" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,17 +0,0 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_22_71)">
|
||||
<path d="M18.4738 0.60614H1.55545C0.803726 0.60614 0.194336 1.21553 0.194336 1.96725V12.1895C0.194336 12.9412 0.803726 13.5506 1.55545 13.5506H18.4738C19.2255 13.5506 19.8349 12.9412 19.8349 12.1895V1.96725C19.8349 1.21553 19.2255 0.60614 18.4738 0.60614Z" fill="white" stroke="#F3F4F6" stroke-width="0.5"/>
|
||||
<mask id="mask0_22_71" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-1" y="0" width="22" height="14">
|
||||
<path d="M18.4738 0.60614H1.55545C0.803726 0.60614 0.194336 1.21553 0.194336 1.96725V12.1895C0.194336 12.9412 0.803726 13.5506 1.55545 13.5506H18.4738C19.2255 13.5506 19.8349 12.9412 19.8349 12.1895V1.96725C19.8349 1.21553 19.2255 0.60614 18.4738 0.60614Z" fill="white" stroke="white" stroke-width="0.5"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_22_71)">
|
||||
<path d="M20.0293 0.411682H13.3528V13.7451H20.0293V0.411682Z" fill="#E43D4C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.7451H6.67651V0.411682H0V13.7451Z" fill="#1BB65D"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_22_71">
|
||||
<rect width="20.2222" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,34 +0,0 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_22_85)">
|
||||
<path d="M18.4774 0.411499H1.55556C0.696446 0.411499 0 1.10794 0 1.96705V12.1893C0 13.0484 0.696446 13.7449 1.55556 13.7449H18.4774C19.3365 13.7449 20.033 13.0484 20.033 12.1893V1.96705C20.033 1.10794 19.3365 0.411499 18.4774 0.411499Z" fill="white"/>
|
||||
<mask id="mask0_22_85" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="21" height="14">
|
||||
<path d="M18.4774 0.411499H1.55556C0.696446 0.411499 0 1.10794 0 1.96705V12.1893C0 13.0484 0.696446 13.7449 1.55556 13.7449H18.4774C19.3365 13.7449 20.033 13.0484 20.033 12.1893V1.96705C20.033 1.10794 19.3365 0.411499 18.4774 0.411499Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_22_85)">
|
||||
<path d="M20.033 0.411499H0V13.7449H20.033V0.411499Z" fill="#0A17A7"/>
|
||||
<path d="M-0.73999 0.152256H5.37038e-05H0.477031H0.55542L0.620678 0.195689L3.89426 2.37448H4.69983L8.45512 0.187479L8.84486 -0.0394897V0.411516V0.717082C8.84486 0.890593 8.75814 1.0526 8.61363 1.14873L8.47005 0.932904L8.61363 1.14874L5.50605 3.21706V3.84234L8.4104 6.16201C8.71365 6.40418 8.54239 6.893 8.15435 6.893C8.07347 6.893 7.99437 6.86908 7.92701 6.82425M-0.73999 0.152256L7.92701 6.82425L-0.73999 0.152256ZM-0.73999 0.152256L-0.161741 0.614093L3.07963 3.20292V3.82819L-0.143593 5.97346L-0.259205 6.05042V6.1893V6.63374V7.08475L0.130526 6.85777L3.88586 4.67077H4.69143L7.92701 6.82425M-0.73999 0.152256L8.07059 6.60843L7.92701 6.82425" fill="#FF2E3B"/>
|
||||
<path d="M-0.73999 0.152256H5.37069e-05H0.477031H0.55542L0.620678 0.195689L3.89426 2.37448H4.69983L8.45512 0.187479L8.84487 -0.0394897V0.411516V0.717082C8.84487 0.890593 8.75814 1.0526 8.61363 1.14873L8.47005 0.932904L8.61363 1.14874L5.50605 3.21706V3.84234L8.4104 6.16201C8.71365 6.40418 8.54239 6.893 8.15435 6.893C8.07347 6.893 7.99437 6.86908 7.92701 6.82425M-0.73999 0.152256L7.92701 6.82425M-0.73999 0.152256L-0.161741 0.614093L3.07963 3.20292V3.82819L-0.143593 5.97346L-0.259205 6.05042V6.1893V6.63374V7.08475L0.130526 6.85777L3.88586 4.67077H4.69143L7.92701 6.82425M-0.73999 0.152256L8.07059 6.60843L7.92701 6.82425" stroke="white" stroke-width="0.666667"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2.63372V4.4115H3.33884V6.55965C3.33884 6.84602 3.57099 7.07816 3.85736 7.07816H4.72822C5.01459 7.07816 5.24674 6.84602 5.24674 6.55965V4.4115H8.54404C8.83042 4.4115 9.06259 4.17935 9.06259 3.89298V3.15224C9.06259 2.86587 8.83042 2.63372 8.54404 2.63372H5.24674V0.411499H3.33884V2.63372H0Z" fill="url(#paint0_linear_22_85)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 3.07816H3.81581V2.63372V0.411499H4.76977V2.63372V3.07816H8.58558V3.96705H4.76977V4.4115V6.63372H3.81581V4.4115V3.96705H0V3.07816Z" fill="url(#paint1_linear_22_85)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.29302 11.3003L3.45195 11.7123L3.61258 10.8397L2.93213 10.2216L3.87249 10.0943L4.29302 9.30029L4.71357 10.0943L5.65392 10.2216L4.97348 10.8397L5.1341 11.7123L4.29302 11.3003Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3096 11.9671L13.635 12.1512L13.8326 11.5226L13.635 10.894L14.3096 11.0781L14.9841 10.894L14.7866 11.5226L14.9841 12.1512L14.3096 11.9671Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3096 3.52257L13.635 3.70667L13.8326 3.07812L13.635 2.44958L14.3096 2.63369L14.9841 2.44958L14.7866 3.07812L14.9841 3.70667L14.3096 3.52257Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1711 6.18932L16.4966 6.37342L16.6941 5.74488L16.4966 5.11633L17.1711 5.30043L17.8457 5.11633L17.6482 5.74488L17.8457 6.37342L17.1711 6.18932Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4475 7.07823L10.7729 7.26232L10.9706 6.63379L10.7729 6.00525L11.4475 6.18934L12.1221 6.00525L11.9245 6.63379L12.1221 7.26232L11.4475 7.07823Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.7402 8.18925L15.4028 8.28134L15.5017 7.96704L15.4028 7.65277L15.7402 7.74482L16.0774 7.65277L15.9786 7.96704L16.0774 8.28134L15.7402 8.18925Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_22_85" x1="0" y1="0.411499" x2="0" y2="7.07816" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F0F0F0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_22_85" x1="0" y1="0.411499" x2="0" y2="6.63372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF2E3B"/>
|
||||
<stop offset="1" stop-color="#FC0D1B"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_22_85">
|
||||
<rect width="20.2222" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.5 KiB |
@ -1,19 +0,0 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_22_107)">
|
||||
<path d="M18.4773 0.605957H1.55545C0.803726 0.605957 0.194336 1.21535 0.194336 1.96707V12.1893C0.194336 12.941 0.803726 13.5504 1.55545 13.5504H18.4773C19.229 13.5504 19.8384 12.941 19.8384 12.1893V1.96707C19.8384 1.21535 19.229 0.605957 18.4773 0.605957Z" fill="white" stroke="#F3F4F6" stroke-width="0.5"/>
|
||||
<mask id="mask0_22_107" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-1" y="0" width="22" height="14">
|
||||
<path d="M18.4773 0.605957H1.55545C0.803726 0.605957 0.194336 1.21535 0.194336 1.96707V12.1893C0.194336 12.941 0.803726 13.5504 1.55545 13.5504H18.4773C19.229 13.5504 19.8384 12.941 19.8384 12.1893V1.96707C19.8384 1.21535 19.229 0.605957 18.4773 0.605957Z" fill="white" stroke="white" stroke-width="0.5"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_22_107)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.85594H20.033V0.411499H0V4.85594Z" fill="#FFA44A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.7447H20.033V9.30029H0V13.7447Z" fill="#1A9F0B"/>
|
||||
<path d="M10.0164 8.67075C10.9323 8.67075 11.7065 7.97472 11.7065 7.07819C11.7065 6.18168 10.9323 5.4856 10.0164 5.4856C9.10045 5.4856 8.32617 6.18168 8.32617 7.07819C8.32617 7.97472 9.10045 8.67075 10.0164 8.67075Z" fill="#181A93" fill-opacity="0.15" stroke="#181A93" stroke-width="0.666667"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0166 7.52262C10.28 7.52262 10.4935 7.32363 10.4935 7.07817C10.4935 6.83271 10.28 6.63373 10.0166 6.63373C9.75313 6.63373 9.53955 6.83271 9.53955 7.07817C9.53955 7.32363 9.75313 7.52262 10.0166 7.52262Z" fill="#181A93"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_22_107">
|
||||
<rect width="20.2222" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 471 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1,53 +0,0 @@
|
||||
<svg width="176" height="32" viewBox="0 0 176 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44 9.17592H50.2773V27.1572H54.032V9.17592H60.3093V5.71459H44V9.17592Z" fill="#1C2434"/>
|
||||
<path d="M64.4999 27.5386C66.7585 27.5386 68.7239 26.5119 69.3399 25.0159L69.6039 27.1572H72.7425V18.3572C72.7425 14.3386 70.3372 12.2266 66.4359 12.2266C62.5052 12.2266 59.9239 14.2799 59.9239 17.4186H62.9745C62.9745 15.8933 64.1479 15.0133 66.2599 15.0133C68.0785 15.0133 69.2519 15.8053 69.2519 17.7706V18.0932L64.9105 18.4159C61.4785 18.6799 59.5425 20.3519 59.5425 23.0213C59.5425 25.7492 61.4199 27.5386 64.4999 27.5386ZM65.6732 24.8399C64.0599 24.8399 63.1799 24.1946 63.1799 22.8746C63.1799 21.7012 64.0305 20.9679 66.2599 20.7626L69.2812 20.5279V21.2906C69.2812 23.5199 67.8732 24.8399 65.6732 24.8399Z" fill="#1C2434"/>
|
||||
<path d="M78.0475 9.76258C79.2209 9.76258 80.1889 8.79458 80.1889 7.59192C80.1889 6.38925 79.2209 5.45059 78.0475 5.45059C76.8155 5.45059 75.8475 6.38925 75.8475 7.59192C75.8475 8.79458 76.8155 9.76258 78.0475 9.76258ZM76.2582 27.1572H79.8369V12.6666H76.2582V27.1572Z" fill="#1C2434"/>
|
||||
<path d="M87.1422 27.1572V5.33325H83.5929V27.1572H87.1422Z" fill="#1C2434"/>
|
||||
<path d="M93.2722 27.1572L95.0029 22.1999H103.011L104.742 27.1572H108.702L100.958 5.71459H97.1149L89.3709 27.1572H93.2722ZM98.5522 12.1093C98.7576 11.5226 98.9335 10.8773 99.0215 10.4666C99.0802 10.9066 99.2855 11.5519 99.4615 12.1093L101.926 19.0319H96.1175L98.5522 12.1093Z" fill="#1C2434"/>
|
||||
<path d="M116.158 27.5386C118.358 27.5386 120.236 26.5706 121.116 24.8986L121.35 27.1572H124.636V5.33325H121.086V14.5146C120.177 13.0773 118.388 12.2266 116.364 12.2266C111.993 12.2266 109.353 15.4533 109.353 19.9706C109.353 24.4586 111.964 27.5386 116.158 27.5386ZM116.95 24.2532C114.457 24.2532 112.932 22.4346 112.932 19.8533C112.932 17.2719 114.457 15.4239 116.95 15.4239C119.444 15.4239 121.057 17.2426 121.057 19.8533C121.057 22.4639 119.444 24.2532 116.95 24.2532Z" fill="#1C2434"/>
|
||||
<path d="M131.944 27.1572V18.9439C131.944 16.5973 133.322 15.4826 135.024 15.4826C136.725 15.4826 137.81 16.5679 137.81 18.5919V27.1572H141.389V18.9439C141.389 16.5679 142.709 15.4533 144.44 15.4533C146.141 15.4533 147.256 16.5386 147.256 18.6213V27.1572H150.805V17.6826C150.805 14.3386 148.869 12.2266 145.349 12.2266C143.149 12.2266 141.448 13.3119 140.714 14.9839C139.952 13.3119 138.426 12.2266 136.226 12.2266C134.144 12.2266 132.677 13.1653 131.944 14.3679L131.65 12.6666H128.365V27.1572H131.944Z" fill="#1C2434"/>
|
||||
<path d="M156.107 9.76258C157.281 9.76258 158.249 8.79458 158.249 7.59192C158.249 6.38925 157.281 5.45059 156.107 5.45059C154.875 5.45059 153.907 6.38925 153.907 7.59192C153.907 8.79458 154.875 9.76258 156.107 9.76258ZM154.318 27.1572H157.897V12.6666H154.318V27.1572Z" fill="#1C2434"/>
|
||||
<path d="M165.173 27.1572V19.3546C165.173 17.0079 166.522 15.4826 168.722 15.4826C170.57 15.4826 171.773 16.6559 171.773 19.0906V27.1572H175.351V18.2399C175.351 14.4853 173.474 12.2266 169.837 12.2266C167.871 12.2266 166.111 13.0773 165.202 14.5439L164.909 12.6666H161.594V27.1572H165.173Z" fill="#1C2434"/>
|
||||
<path d="M0 8.42105C0 3.77023 3.77023 0 8.42105 0H23.5789C28.2298 0 32 3.77023 32 8.42105V23.5789C32 28.2298 28.2298 32 23.5789 32H8.42105C3.77023 32 0 28.2298 0 23.5789V8.42105Z" fill="#3C50E0"/>
|
||||
<g filter="url(#filter0_d_521_14075)">
|
||||
<path d="M8.42139 8.42127C8.42139 7.49111 9.17543 6.73706 10.1056 6.73706V6.73706C11.0358 6.73706 11.7898 7.49111 11.7898 8.42127V23.5792C11.7898 24.5093 11.0358 25.2634 10.1056 25.2634V25.2634C9.17543 25.2634 8.42139 24.5093 8.42139 23.5792V8.42127Z" fill="white"/>
|
||||
</g>
|
||||
<g opacity="0.9" filter="url(#filter1_d_521_14075)">
|
||||
<path d="M14.7368 15.1576C14.7368 14.2274 15.4909 13.4734 16.421 13.4734V13.4734C17.3512 13.4734 18.1052 14.2274 18.1052 15.1576V23.5786C18.1052 24.5088 17.3512 25.2629 16.421 25.2629V25.2629C15.4909 25.2629 14.7368 24.5088 14.7368 23.5786V15.1576Z" fill="white"/>
|
||||
</g>
|
||||
<g opacity="0.7" filter="url(#filter2_d_521_14075)">
|
||||
<path d="M21.0522 10.9469C21.0522 10.0167 21.8063 9.2627 22.7365 9.2627V9.2627C23.6666 9.2627 24.4207 10.0167 24.4207 10.9469V23.5785C24.4207 24.5086 23.6666 25.2627 22.7365 25.2627V25.2627C21.8063 25.2627 21.0522 24.5086 21.0522 23.5785V10.9469Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_521_14075" x="7.42139" y="6.23706" width="5.36865" height="20.5264" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_521_14075"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_521_14075" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_521_14075" x="13.7368" y="12.9734" width="5.36865" height="13.7896" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_521_14075"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_521_14075" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_521_14075" x="20.0522" y="8.7627" width="5.36865" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_521_14075"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_521_14075" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 6.2 KiB |
@ -1,44 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8.42105C0 3.77023 3.77023 0 8.42105 0H23.5789C28.2298 0 32 3.77023 32 8.42105V23.5789C32 28.2298 28.2298 32 23.5789 32H8.42105C3.77023 32 0 28.2298 0 23.5789V8.42105Z" fill="#3C50E0"/>
|
||||
<g filter="url(#filter0_d_521_14078)">
|
||||
<path d="M8.42139 8.42127C8.42139 7.49111 9.17543 6.73706 10.1056 6.73706V6.73706C11.0358 6.73706 11.7898 7.49111 11.7898 8.42127V23.5792C11.7898 24.5093 11.0358 25.2634 10.1056 25.2634V25.2634C9.17543 25.2634 8.42139 24.5093 8.42139 23.5792V8.42127Z" fill="white"/>
|
||||
</g>
|
||||
<g opacity="0.9" filter="url(#filter1_d_521_14078)">
|
||||
<path d="M14.7368 15.1576C14.7368 14.2274 15.4909 13.4734 16.421 13.4734V13.4734C17.3512 13.4734 18.1052 14.2274 18.1052 15.1576V23.5786C18.1052 24.5088 17.3512 25.2629 16.421 25.2629V25.2629C15.4909 25.2629 14.7368 24.5088 14.7368 23.5786V15.1576Z" fill="white"/>
|
||||
</g>
|
||||
<g opacity="0.7" filter="url(#filter2_d_521_14078)">
|
||||
<path d="M21.0522 10.9469C21.0522 10.0167 21.8063 9.2627 22.7365 9.2627V9.2627C23.6666 9.2627 24.4207 10.0167 24.4207 10.9469V23.5785C24.4207 24.5086 23.6666 25.2627 22.7365 25.2627V25.2627C21.8063 25.2627 21.0522 24.5086 21.0522 23.5785V10.9469Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_521_14078" x="7.42139" y="6.23706" width="5.36865" height="20.5264" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_521_14078"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_521_14078" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_521_14078" x="13.7368" y="12.9734" width="5.36865" height="13.7896" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_521_14078"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_521_14078" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_521_14078" x="20.0522" y="8.7627" width="5.36865" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_521_14078"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_521_14078" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.3 KiB |
@ -1,53 +0,0 @@
|
||||
<svg width="176" height="32" viewBox="0 0 176 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44 9.17592H50.2773V27.1572H54.032V9.17592H60.3093V5.71459H44V9.17592Z" fill="white"/>
|
||||
<path d="M64.4999 27.5386C66.7585 27.5386 68.7239 26.5119 69.3399 25.0159L69.6039 27.1572H72.7425V18.3572C72.7425 14.3386 70.3372 12.2266 66.4359 12.2266C62.5052 12.2266 59.9239 14.2799 59.9239 17.4186H62.9745C62.9745 15.8933 64.1479 15.0133 66.2599 15.0133C68.0785 15.0133 69.2519 15.8053 69.2519 17.7706V18.0932L64.9105 18.4159C61.4785 18.6799 59.5425 20.3519 59.5425 23.0213C59.5425 25.7492 61.4199 27.5386 64.4999 27.5386ZM65.6732 24.8399C64.0599 24.8399 63.1799 24.1946 63.1799 22.8746C63.1799 21.7012 64.0305 20.9679 66.2599 20.7626L69.2812 20.5279V21.2906C69.2812 23.5199 67.8732 24.8399 65.6732 24.8399Z" fill="white"/>
|
||||
<path d="M78.0475 9.76258C79.2209 9.76258 80.1889 8.79458 80.1889 7.59192C80.1889 6.38925 79.2209 5.45059 78.0475 5.45059C76.8155 5.45059 75.8475 6.38925 75.8475 7.59192C75.8475 8.79458 76.8155 9.76258 78.0475 9.76258ZM76.2582 27.1572H79.8369V12.6666H76.2582V27.1572Z" fill="white"/>
|
||||
<path d="M87.1422 27.1572V5.33325H83.5929V27.1572H87.1422Z" fill="white"/>
|
||||
<path d="M93.2722 27.1572L95.0029 22.1999H103.011L104.742 27.1572H108.702L100.958 5.71459H97.1149L89.3709 27.1572H93.2722ZM98.5522 12.1093C98.7576 11.5226 98.9335 10.8773 99.0215 10.4666C99.0802 10.9066 99.2855 11.5519 99.4615 12.1093L101.926 19.0319H96.1175L98.5522 12.1093Z" fill="white"/>
|
||||
<path d="M116.158 27.5386C118.358 27.5386 120.236 26.5706 121.116 24.8986L121.35 27.1572H124.636V5.33325H121.086V14.5146C120.177 13.0773 118.388 12.2266 116.364 12.2266C111.993 12.2266 109.353 15.4533 109.353 19.9706C109.353 24.4586 111.964 27.5386 116.158 27.5386ZM116.95 24.2532C114.457 24.2532 112.932 22.4346 112.932 19.8533C112.932 17.2719 114.457 15.4239 116.95 15.4239C119.444 15.4239 121.057 17.2426 121.057 19.8533C121.057 22.4639 119.444 24.2532 116.95 24.2532Z" fill="white"/>
|
||||
<path d="M131.944 27.1572V18.9439C131.944 16.5973 133.322 15.4826 135.024 15.4826C136.725 15.4826 137.81 16.5679 137.81 18.5919V27.1572H141.389V18.9439C141.389 16.5679 142.709 15.4533 144.44 15.4533C146.141 15.4533 147.256 16.5386 147.256 18.6213V27.1572H150.805V17.6826C150.805 14.3386 148.869 12.2266 145.349 12.2266C143.149 12.2266 141.448 13.3119 140.714 14.9839C139.952 13.3119 138.426 12.2266 136.226 12.2266C134.144 12.2266 132.677 13.1653 131.944 14.3679L131.65 12.6666H128.365V27.1572H131.944Z" fill="white"/>
|
||||
<path d="M156.107 9.76258C157.281 9.76258 158.249 8.79458 158.249 7.59192C158.249 6.38925 157.281 5.45059 156.107 5.45059C154.875 5.45059 153.907 6.38925 153.907 7.59192C153.907 8.79458 154.875 9.76258 156.107 9.76258ZM154.318 27.1572H157.897V12.6666H154.318V27.1572Z" fill="white"/>
|
||||
<path d="M165.173 27.1572V19.3546C165.173 17.0079 166.522 15.4826 168.722 15.4826C170.57 15.4826 171.773 16.6559 171.773 19.0906V27.1572H175.351V18.2399C175.351 14.4853 173.474 12.2266 169.837 12.2266C167.871 12.2266 166.111 13.0773 165.202 14.5439L164.909 12.6666H161.594V27.1572H165.173Z" fill="white"/>
|
||||
<path d="M0 8.42105C0 3.77023 3.77023 0 8.42105 0H23.5789C28.2298 0 32 3.77023 32 8.42105V23.5789C32 28.2298 28.2298 32 23.5789 32H8.42105C3.77023 32 0 28.2298 0 23.5789V8.42105Z" fill="#3C50E0"/>
|
||||
<g filter="url(#filter0_d_506_10589)">
|
||||
<path d="M8.42139 8.42127C8.42139 7.49111 9.17543 6.73706 10.1056 6.73706V6.73706C11.0358 6.73706 11.7898 7.49111 11.7898 8.42127V23.5792C11.7898 24.5093 11.0358 25.2634 10.1056 25.2634V25.2634C9.17543 25.2634 8.42139 24.5093 8.42139 23.5792V8.42127Z" fill="white"/>
|
||||
</g>
|
||||
<g opacity="0.9" filter="url(#filter1_d_506_10589)">
|
||||
<path d="M14.7368 15.1576C14.7368 14.2274 15.4909 13.4734 16.421 13.4734V13.4734C17.3512 13.4734 18.1052 14.2274 18.1052 15.1576V23.5786C18.1052 24.5088 17.3512 25.2629 16.421 25.2629V25.2629C15.4909 25.2629 14.7368 24.5088 14.7368 23.5786V15.1576Z" fill="white"/>
|
||||
</g>
|
||||
<g opacity="0.7" filter="url(#filter2_d_506_10589)">
|
||||
<path d="M21.0522 10.9469C21.0522 10.0167 21.8063 9.2627 22.7365 9.2627V9.2627C23.6666 9.2627 24.4207 10.0167 24.4207 10.9469V23.5785C24.4207 24.5086 23.6666 25.2627 22.7365 25.2627V25.2627C21.8063 25.2627 21.0522 24.5086 21.0522 23.5785V10.9469Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_506_10589" x="7.42139" y="6.23706" width="5.36865" height="20.5264" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_506_10589"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_506_10589" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_506_10589" x="13.7368" y="12.9734" width="5.36865" height="13.7896" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_506_10589"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_506_10589" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_506_10589" x="20.0522" y="8.7627" width="5.36865" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_506_10589"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_506_10589" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 7.5 KiB |
@ -1,53 +0,0 @@
|
||||
const Drag = (): void => {
|
||||
const draggables = document.querySelectorAll<HTMLElement>('.task');
|
||||
const droppables = document.querySelectorAll<HTMLElement>('.swim-lane');
|
||||
|
||||
draggables.forEach((task) => {
|
||||
task.addEventListener('dragstart', () => {
|
||||
task.classList.add('is-dragging');
|
||||
});
|
||||
task.addEventListener('dragend', () => {
|
||||
task.classList.remove('is-dragging');
|
||||
});
|
||||
});
|
||||
|
||||
droppables.forEach((zone) => {
|
||||
zone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const bottomTask = insertAboveTask(zone, e.clientY);
|
||||
const curTask = document.querySelector('.is-dragging') as HTMLElement;
|
||||
|
||||
if (!bottomTask) {
|
||||
zone.appendChild(curTask);
|
||||
} else {
|
||||
zone.insertBefore(curTask, bottomTask);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const insertAboveTask = (
|
||||
zone: HTMLElement,
|
||||
mouseY: number
|
||||
): HTMLElement | null => {
|
||||
const els = zone.querySelectorAll<HTMLElement>('.task:not(.is-dragging)');
|
||||
|
||||
let closestTask: HTMLElement | null = null;
|
||||
let closestOffset = Number.NEGATIVE_INFINITY;
|
||||
|
||||
els.forEach((task) => {
|
||||
const { top } = task.getBoundingClientRect();
|
||||
|
||||
const offset = mouseY - top;
|
||||
|
||||
if (offset < 0 && offset > closestOffset) {
|
||||
closestOffset = offset;
|
||||
closestTask = task;
|
||||
}
|
||||
});
|
||||
|
||||
return closestTask;
|
||||
};
|
||||
};
|
||||
|
||||
export default Drag;
|
@ -12,10 +12,10 @@ const ECommerce = () => {
|
||||
const { data: stats, isLoading, error } = useQuery(getDailyStats);
|
||||
|
||||
return (
|
||||
<DefaultLayout >
|
||||
<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 />
|
||||
<TotalRevenueCard dailyStats={stats?.dailyStats} weeklyStats={stats?.weeklyStats} isLoading={isLoading}/>
|
||||
<TotalRevenueCard dailyStats={stats?.dailyStats} weeklyStats={stats?.weeklyStats} isLoading={isLoading} />
|
||||
<TotalPayingUsersCard dailyStats={stats?.dailyStats} isLoading={isLoading} />
|
||||
<TotalSignupsCard dailyStats={stats?.dailyStats} isLoading={isLoading} />
|
||||
</div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { FormEvent } from 'react';
|
||||
import Breadcrumb from '../components/Breadcrumb';
|
||||
import userThree from '../images/user/user-03.png';
|
||||
import toast from 'react-hot-toast';
|
||||
import DefaultLayout from '../layout/DefaultLayout';
|
||||
|
||||
@ -235,7 +234,7 @@ const Settings = () => {
|
||||
<form action="#">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-14 w-14 rounded-full">
|
||||
<img src={userThree} alt="User" />
|
||||
{/* <img src={userThree} alt="User" /> */}
|
||||
</div>
|
||||
<div>
|
||||
<span className="mb-1.5 text-black dark:text-white">
|
||||
|
@ -1,15 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { RelatedObject } from '@wasp/entities';
|
||||
import generateGptResponse from '@wasp/actions/generateGptResponse';
|
||||
import useAuth from '@wasp/auth/useAuth';
|
||||
|
||||
type GptPayload = {
|
||||
instructions: string;
|
||||
command: string;
|
||||
temperature: number;
|
||||
};
|
||||
|
||||
export default function GptPage() {
|
||||
const [temperature, setTemperature] = useState<number>(1);
|
||||
const [response, setResponse] = useState<string>('');
|
||||
@ -23,7 +16,7 @@ export default function GptPage() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await generateGptResponse({ instructions, command, temperature })
|
||||
const response = await generateGptResponse({ instructions, command, temperature });
|
||||
if (response) {
|
||||
setResponse(response.content);
|
||||
}
|
||||
@ -67,7 +60,7 @@ export default function GptPage() {
|
||||
/>
|
||||
</div>
|
||||
<span className='text-sm text-red-500'>
|
||||
{formErrors.instructions && formErrors.instructions.message}
|
||||
{typeof formErrors?.instructions?.message === 'string' ? formErrors.instructions.message : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className='col-span-full'>
|
||||
@ -90,7 +83,9 @@ export default function GptPage() {
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<span className='text-sm text-red-500'>{formErrors.command && formErrors.command.message}</span>
|
||||
<span className='text-sm text-red-500'>
|
||||
{typeof formErrors?.command?.message === 'string' ? formErrors.command.message : null}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='h-10 '>
|
@ -1,10 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { AiOutlineBars, AiOutlineClose, AiOutlineUser } from 'react-icons/ai';
|
||||
import { BiLogIn } from 'react-icons/bi';
|
||||
import { HiBars3 } from 'react-icons/hi2';
|
||||
import useAuth from '@wasp/auth/useAuth';
|
||||
import logo from './static/logo.png';
|
||||
import DropdownUser from './common/DropdownUser';
|
||||
import logo from '../static/logo.png';
|
||||
import DropdownUser from './DropdownUser';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'GPT Wrapper', href: '/gpt' },
|
||||
@ -12,7 +11,7 @@ const navigation = [
|
||||
{ name: 'Blog', href: 'https://saas-template.gitbook.io/posts/' },
|
||||
];
|
||||
|
||||
export default function NavBar() {
|
||||
export default function AppNavBar() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const { data: user, isLoading: isUserLoading } = useAuth();
|
||||
@ -46,18 +45,18 @@ export default function NavBar() {
|
||||
))}
|
||||
</div>
|
||||
<div className='hidden lg:flex lg:flex-1 lg:justify-end lg:align-end'>
|
||||
<a
|
||||
href={!user ? '/login' : '/account'}
|
||||
className='flex justify-end items-center text-sm font-semibold leading-6 '
|
||||
>
|
||||
{isUserLoading ? null : !user ? (
|
||||
{isUserLoading ? null : !user ? (
|
||||
<a
|
||||
href={!user ? '/login' : '/account'}
|
||||
className='flex justify-end items-center text-sm font-semibold leading-6 '
|
||||
>
|
||||
<div className='duration-300 ease-in-out text-gray-900 hover:text-yellow-500'>
|
||||
Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
|
||||
</div>
|
||||
) : (
|
||||
<DropdownUser username={user.email?.split('@')[0]} />
|
||||
)}
|
||||
</a>
|
||||
</a>
|
||||
) : (
|
||||
<DropdownUser username={user.email?.split('@')[0]} isUserAdmin={user.isAdmin} />
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
@ -1,25 +1,25 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom'; // TODO change all Links to wasp router links
|
||||
import { CgProfile } from 'react-icons/cg';
|
||||
import { MdOutlineSpaceDashboard } from 'react-icons/md';
|
||||
import { TfiDashboard } from 'react-icons/tfi'
|
||||
import { TfiDashboard } from 'react-icons/tfi';
|
||||
import logout from '@wasp/auth/logout';
|
||||
|
||||
const DropdownUser = ({username} : {username: string | undefined}) => {
|
||||
const DropdownUser = ({ username, isUserAdmin }: { username: string | undefined, isUserAdmin: boolean }) => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
const trigger = useRef<any>(null);
|
||||
const dropdown = useRef<any>(null);
|
||||
|
||||
const toggleDropdown = () => setDropdownOpen((prev) => !prev);
|
||||
|
||||
// close on click outside
|
||||
useEffect(() => {
|
||||
const clickHandler = ({ target }: MouseEvent) => {
|
||||
if (!dropdown.current) return;
|
||||
if (
|
||||
!dropdownOpen ||
|
||||
dropdown.current.contains(target) ||
|
||||
trigger.current.contains(target)
|
||||
)
|
||||
if (!dropdown.current) return
|
||||
if (!dropdownOpen || dropdown.current.contains(target) || trigger.current.contains(target)) {
|
||||
return;
|
||||
}
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
document.addEventListener('click', clickHandler);
|
||||
@ -38,11 +38,10 @@ const DropdownUser = ({username} : {username: string | undefined}) => {
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<Link
|
||||
<button
|
||||
ref={trigger}
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
onClick={toggleDropdown}
|
||||
className='flex items-center gap-4 duration-300 ease-in-out text-gray-900 hover:text-yellow-500'
|
||||
to='#'
|
||||
>
|
||||
<span className='hidden text-right lg:block'>
|
||||
<span className='block text-sm font-medium dark:text-white'>{username}</span>
|
||||
@ -63,36 +62,25 @@ const DropdownUser = ({username} : {username: string | undefined}) => {
|
||||
fill=''
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</button>
|
||||
|
||||
{/* <!-- Dropdown Start --> */}
|
||||
<div
|
||||
ref={dropdown}
|
||||
onFocus={() => setDropdownOpen(true)}
|
||||
onBlur={() => setDropdownOpen(false)}
|
||||
className={`absolute right-0 mt-4 flex w-62.5 flex-col rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark ${
|
||||
dropdownOpen === true ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<ul className='flex flex-col gap-5 border-b border-stroke px-6 py-5 dark:border-strokedark'>
|
||||
<ul className='flex flex-col gap-5 border-b border-stroke px-6 py-4 dark:border-strokedark'>
|
||||
<li>
|
||||
<Link
|
||||
to='/gpt'
|
||||
className='flex items-center gap-3.5 font-medium duration-300 ease-in-out hover:text-yellow-500'
|
||||
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'
|
||||
>
|
||||
<MdOutlineSpaceDashboard size='1.1rem' />
|
||||
App
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to='/admin'
|
||||
className='flex items-center gap-3.5 font-medium duration-300 ease-in-out hover:text-yellow-500'
|
||||
>
|
||||
<TfiDashboard size='1.1rem' />
|
||||
Admin Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to='/account'
|
||||
@ -119,7 +107,21 @@ const DropdownUser = ({username} : {username: string | undefined}) => {
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<button className='flex items-center gap-3.5 py-4 px-6 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'>
|
||||
{isUserAdmin && <ul className='flex flex-col gap-5 border-b border-stroke px-6 py-4 dark:border-strokedark'>
|
||||
<li className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'>
|
||||
<Link
|
||||
to='/admin'
|
||||
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'
|
||||
>
|
||||
<TfiDashboard size='1.1rem' />
|
||||
Admin Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
</ul>}
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
className='flex items-center gap-3.5 py-4 px-6 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'
|
||||
>
|
||||
<svg
|
||||
className='fill-current'
|
||||
width='18'
|
@ -10,12 +10,18 @@ export function useReferrer() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const refValue = urlParams.get(REFERRER_KEY);
|
||||
|
||||
const [referrer, setReferrer] = useLocalStorage(REFERRER_KEY, refValue);
|
||||
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(refValue);
|
||||
setReferrer(values);
|
||||
}
|
||||
history.replace({
|
||||
search: '',
|
||||
|
@ -3,11 +3,12 @@ import { Dialog } from '@headlessui/react';
|
||||
import { AiFillCheckCircle, AiFillCloseCircle } from 'react-icons/ai';
|
||||
import { HiBars3 } from 'react-icons/hi2';
|
||||
import { BiLogIn } from 'react-icons/bi';
|
||||
import { Link } from '@wasp/router';
|
||||
import logo from '../static/logo.png';
|
||||
import daBoi from '../static/magic-app-gen-logo.png';
|
||||
import { features, navigation, tiers, faqs, footerNavigation } from './contentSections';
|
||||
import useAuth from '@wasp/auth/useAuth';
|
||||
import DropdownUser from '../common/DropdownUser';
|
||||
import DropdownUser from '../components/DropdownUser';
|
||||
|
||||
export default function LandingPage() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
@ -46,18 +47,17 @@ export default function LandingPage() {
|
||||
))}
|
||||
</div>
|
||||
<div className='hidden lg:flex lg:flex-1 lg:justify-end lg:align-end'>
|
||||
<a
|
||||
href={!user ? '/login' : '/account'}
|
||||
className='flex justify-end items-center text-sm font-semibold leading-6 '
|
||||
>
|
||||
<div className='text-sm font-semibold leading-6 '>
|
||||
{isUserLoading ? null : !user ? (
|
||||
<div className='duration-300 ease-in-out text-gray-900 hover:text-yellow-500'>
|
||||
Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
|
||||
</div>
|
||||
<Link to='/login'>
|
||||
<div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500'>
|
||||
Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<DropdownUser username={user.email?.split('@')[0]} />
|
||||
<DropdownUser username={user.email?.split('@')[0]} isUserAdmin={user.isAdmin} />
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<Dialog as='div' className='lg:hidden' open={mobileMenuOpen} onClose={setMobileMenuOpen}>
|
||||
|
Before Width: | Height: | Size: 446 KiB After Width: | Height: | Size: 446 KiB |
@ -3,7 +3,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 { UpdateUser } from '@wasp/actions/types';
|
||||
import { UpdateCurrentUser, SaveReferrer, UpdateUserReferrer, UpdateUserById } from '@wasp/actions/types';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
@ -149,16 +149,77 @@ export const generateGptResponse: GenerateGptResponse<GptPayload, RelatedObject>
|
||||
throw new HttpError(500, 'Something went wrong');
|
||||
};
|
||||
|
||||
export const updateUser: UpdateUser<Partial<User>, User> = async (user, context) => {
|
||||
export const updateUserById: UpdateUserById<{ id: number; data: Partial<User> }, User> = async (
|
||||
{ id, data },
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
if (!context.user.isAdmin) {
|
||||
throw new HttpError(403);
|
||||
}
|
||||
|
||||
const updatedUser = await context.entities.User.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
console.log('updated user', updatedUser.id)
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
export const updateCurrentUser: UpdateCurrentUser<Partial<User>, User> = async (user, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
console.log('updating user', user);
|
||||
|
||||
return context.entities.User.update({
|
||||
where: {
|
||||
id: context.user.id,
|
||||
},
|
||||
data: user
|
||||
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,6 +1,6 @@
|
||||
import HttpError from '@wasp/core/HttpError.js';
|
||||
import type { DailyStats, RelatedObject } from '@wasp/entities';
|
||||
import type { GetRelatedObjects, GetDailyStats } from '@wasp/queries/types';
|
||||
import type { DailyStats, RelatedObject, Referrer, User } from '@wasp/entities';
|
||||
import type { GetRelatedObjects, GetDailyStats, GetReferrerStats, GetPaginatedUsers } from '@wasp/queries/types';
|
||||
|
||||
type DailyStatsValues = {
|
||||
dailyStats: DailyStats;
|
||||
@ -37,5 +37,96 @@ export const getDailyStats: GetDailyStats<void, DailyStatsValues> = async (_args
|
||||
take: 7,
|
||||
});
|
||||
|
||||
return {dailyStats, weeklyStats};
|
||||
}
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
return referrers.map((referrer) => ({
|
||||
...referrer,
|
||||
users: referrer.users.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
hasPaid: user.hasPaid,
|
||||
subscriptionStatus: user.subscriptionStatus,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
type GetPaginatedUsersInput = {
|
||||
skip: number;
|
||||
cursor?: number | undefined;
|
||||
emailContains?: string;
|
||||
subscriptionStatus?: string[]
|
||||
};
|
||||
type GetPaginatedUsersOutput = {
|
||||
users: Pick<User, 'id' | 'email' | 'lastActiveTimestamp' | 'hasPaid' | 'subscriptionStatus' | 'stripeId'>[];
|
||||
totalPages: number;
|
||||
};
|
||||
|
||||
export const getPaginatedUsers: GetPaginatedUsers<GetPaginatedUsersInput, GetPaginatedUsersOutput> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
|
||||
let hasPaid = undefined
|
||||
if (!!args.subscriptionStatus && args.subscriptionStatus.includes('hasPaid')) {
|
||||
hasPaid = true
|
||||
}
|
||||
|
||||
let subscriptionStatus = args.subscriptionStatus?.filter((status) => status !== 'hasPaid')
|
||||
subscriptionStatus = subscriptionStatus?.length ? subscriptionStatus : undefined
|
||||
|
||||
const queryResults = await context.entities.User.findMany({
|
||||
skip: args.skip,
|
||||
take: 10,
|
||||
where: {
|
||||
email: {
|
||||
contains: args.emailContains || undefined,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
hasPaid,
|
||||
subscriptionStatus: {
|
||||
in: subscriptionStatus || undefined,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
lastActiveTimestamp: true,
|
||||
hasPaid: true,
|
||||
subscriptionStatus: true,
|
||||
stripeId: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
const totalUserCount = await context.entities.User.count({
|
||||
where: {
|
||||
email: {
|
||||
contains: args.emailContains || undefined,
|
||||
},
|
||||
hasPaid,
|
||||
subscriptionStatus: {
|
||||
in: subscriptionStatus || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
const totalPages = Math.ceil(totalUserCount / 10);
|
||||
|
||||
return {
|
||||
users: queryResults,
|
||||
totalPages
|
||||
};
|
||||
};
|
||||
|
72
src/server/scripts/usersSeed.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
import type { User, Referrer } 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> = {
|
||||
id: ++prevUserId,
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password({
|
||||
length: 12,
|
||||
prefix: 'Aa1!'
|
||||
}),
|
||||
createdAt: faker.date.between({ from: new Date('2023-01-01'), to: new Date() }),
|
||||
lastActiveTimestamp: faker.date.recent(),
|
||||
isAdmin: false,
|
||||
isEmailVerified: faker.helpers.arrayElement([true, false]),
|
||||
stripeId: `cus_${faker.string.uuid()}`,
|
||||
hasPaid: faker.helpers.arrayElement([true, false]),
|
||||
sendEmail: false,
|
||||
subscriptionStatus: faker.helpers.arrayElement(['active', 'canceled', 'past_due']),
|
||||
datePaid: faker.date.recent(),
|
||||
credits: faker.number.int({ min: 0, max: 3 }),
|
||||
referrerId: faker.number.int({ min: 1, max: 3 }),
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
||||
const USERS: Partial<User>[] = faker.helpers.multiple(createRandomUser, {
|
||||
count: 50,
|
||||
});
|
||||
|
||||
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({
|
||||
data: user,
|
||||
});
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
@ -6,14 +6,20 @@ const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
});
|
||||
|
||||
export const calculateDailyStats: DailyStats<never, void> = async (_args, context) => {
|
||||
const currentDate = new Date();
|
||||
const yesterdaysDate = new Date(new Date().setDate(currentDate.getDate() - 1));
|
||||
const nowUTC = new Date(Date.now());
|
||||
nowUTC.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const yesterdayUTC = new Date(nowUTC);
|
||||
yesterdayUTC.setUTCDate(yesterdayUTC.getUTCDate() - 1);
|
||||
|
||||
console.log('yesterdayUTC: ', yesterdayUTC);
|
||||
console.log('nowUTC: ', nowUTC);
|
||||
|
||||
try {
|
||||
const yesterdaysStats = await context.entities.DailyStats.findFirst({
|
||||
where: {
|
||||
date: {
|
||||
equals: yesterdaysDate,
|
||||
equals: yesterdayUTC,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -33,16 +39,16 @@ export const calculateDailyStats: DailyStats<never, void> = async (_args, contex
|
||||
if (yesterdaysStats) {
|
||||
userDelta -= yesterdaysStats.userCount;
|
||||
paidUserDelta -= yesterdaysStats.paidUserCount;
|
||||
}
|
||||
}
|
||||
|
||||
const newRunningTotal = await calculateTotalRevenue(context);
|
||||
|
||||
await context.entities.DailyStats.upsert({
|
||||
where: {
|
||||
date: currentDate,
|
||||
date: nowUTC,
|
||||
},
|
||||
create: {
|
||||
date: currentDate,
|
||||
date: nowUTC,
|
||||
userCount,
|
||||
paidUserCount,
|
||||
userDelta,
|
||||
@ -57,24 +63,39 @@ export const calculateDailyStats: DailyStats<never, void> = async (_args, contex
|
||||
totalRevenue: newRunningTotal,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
await context.entities.Logs.create({
|
||||
data: {
|
||||
message: `Daily stats calculated for ${nowUTC.toDateString()}`,
|
||||
level: 'job-info',
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error calculating daily stats: ', error);
|
||||
await context.entities.Logs.create({
|
||||
data: {
|
||||
message: `Error calculating daily stats: ${error?.message}`,
|
||||
level: 'job-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchDailyStripeRevenue() {
|
||||
const startOfDay = new Date();
|
||||
startOfDay.setHours(0, 0, 0, 0); // Sets to beginning of day
|
||||
const startOfDayTimestamp = Math.floor(startOfDay.getTime() / 1000); // Convert to Unix timestamp in seconds
|
||||
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
|
||||
|
||||
const endOfDay = new Date();
|
||||
endOfDay.setHours(23, 59, 59, 999); // Sets to end of day
|
||||
const endOfDayTimestamp = Math.floor(endOfDay.getTime() / 1000); // Convert to Unix timestamp in seconds
|
||||
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 nextPageCursor = undefined;
|
||||
const allPayments = [] as Stripe.Invoice[];
|
||||
|
||||
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,
|
||||
@ -106,13 +127,22 @@ 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());
|
||||
|
||||
const lastTotalEntry = await context.entities.DailyStats.find({
|
||||
// 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 is yesterday
|
||||
date: {
|
||||
equals: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
},
|
||||
date: yesterdayUTC, // Pass the Date object directly, not as a string
|
||||
},
|
||||
});
|
||||
|
||||
|