From 314a8b8a63463c76f654351dbb0b4e5cd0d86cd8 Mon Sep 17 00:00:00 2001
From: vincanger <70215737+vincanger@users.noreply.github.com>
Date: Mon, 13 Nov 2023 19:27:55 +0100
Subject: [PATCH] add admin dashboard
---
main.wasp | 79 ++-
.../migration.sql | 3 +
.../20231113113101_referrer/migration.sql | 14 +
src/client/App.tsx | 51 +-
src/client/Main.css | 152 +++++
src/client/admin/common/Loader/index.tsx | 9 +
src/client/admin/components/BarChart.tsx | 146 +++++
src/client/admin/components/Breadcrumb.tsx | 24 +
src/client/admin/components/CheckboxOne.tsx | 41 ++
src/client/admin/components/CheckboxTwo.tsx | 50 ++
.../components/DailyActiveUsersChart.tsx | 192 +++++++
.../admin/components/DarkModeSwitcher.tsx | 65 +++
src/client/admin/components/DataStats.tsx | 102 ++++
.../admin/components/DropdownEditDelete.tsx | 123 ++++
.../admin/components/DropdownMessage.tsx | 48 ++
src/client/admin/components/DropdownUser.tsx | 138 +++++
src/client/admin/components/Header.tsx | 119 ++++
src/client/admin/components/PieChart.tsx | 150 +++++
src/client/admin/components/ReferrerTable.tsx | 176 ++++++
src/client/admin/components/Sidebar.tsx | 529 ++++++++++++++++++
.../admin/components/SidebarLinkGroup.tsx | 21 +
src/client/admin/components/SwitcherOne.tsx | 33 ++
src/client/admin/components/SwitcherTwo.tsx | 66 +++
.../admin/components/TotalPaidViewsCard.tsx | 53 ++
.../admin/components/TotalPayingUsersCard.tsx | 53 ++
.../admin/components/TotalProfitCard.tsx | 57 ++
.../admin/components/TotalSignupsCard.tsx | 57 ++
src/client/admin/components/UsersTable.tsx | 62 ++
src/client/admin/fonts/Satoshi-Black.eot | Bin 0 -> 73352 bytes
src/client/admin/fonts/Satoshi-Black.ttf | Bin 0 -> 73176 bytes
src/client/admin/fonts/Satoshi-Black.woff | Bin 0 -> 30376 bytes
src/client/admin/fonts/Satoshi-Black.woff2 | Bin 0 -> 23484 bytes
.../admin/fonts/Satoshi-BlackItalic.eot | Bin 0 -> 75950 bytes
.../admin/fonts/Satoshi-BlackItalic.ttf | Bin 0 -> 75760 bytes
.../admin/fonts/Satoshi-BlackItalic.woff | Bin 0 -> 31364 bytes
.../admin/fonts/Satoshi-BlackItalic.woff2 | Bin 0 -> 24276 bytes
src/client/admin/fonts/Satoshi-Bold.eot | Bin 0 -> 73532 bytes
src/client/admin/fonts/Satoshi-Bold.ttf | Bin 0 -> 73368 bytes
src/client/admin/fonts/Satoshi-Bold.woff | Bin 0 -> 32972 bytes
src/client/admin/fonts/Satoshi-Bold.woff2 | Bin 0 -> 25328 bytes
src/client/admin/fonts/Satoshi-BoldItalic.eot | Bin 0 -> 76620 bytes
src/client/admin/fonts/Satoshi-BoldItalic.ttf | Bin 0 -> 76452 bytes
.../admin/fonts/Satoshi-BoldItalic.woff | Bin 0 -> 34336 bytes
.../admin/fonts/Satoshi-BoldItalic.woff2 | Bin 0 -> 26300 bytes
src/client/admin/fonts/Satoshi-Italic.eot | Bin 0 -> 76762 bytes
src/client/admin/fonts/Satoshi-Italic.ttf | Bin 0 -> 76604 bytes
src/client/admin/fonts/Satoshi-Italic.woff | Bin 0 -> 34336 bytes
src/client/admin/fonts/Satoshi-Italic.woff2 | Bin 0 -> 26456 bytes
src/client/admin/fonts/Satoshi-Light.eot | Bin 0 -> 71860 bytes
src/client/admin/fonts/Satoshi-Light.ttf | Bin 0 -> 71684 bytes
src/client/admin/fonts/Satoshi-Light.woff | Bin 0 -> 29276 bytes
src/client/admin/fonts/Satoshi-Light.woff2 | Bin 0 -> 22800 bytes
.../admin/fonts/Satoshi-LightItalic.eot | Bin 0 -> 75590 bytes
.../admin/fonts/Satoshi-LightItalic.ttf | Bin 0 -> 75400 bytes
.../admin/fonts/Satoshi-LightItalic.woff | Bin 0 -> 30336 bytes
.../admin/fonts/Satoshi-LightItalic.woff2 | Bin 0 -> 23408 bytes
src/client/admin/fonts/Satoshi-Medium.eot | Bin 0 -> 73934 bytes
src/client/admin/fonts/Satoshi-Medium.ttf | Bin 0 -> 73756 bytes
src/client/admin/fonts/Satoshi-Medium.woff | Bin 0 -> 33272 bytes
src/client/admin/fonts/Satoshi-Medium.woff2 | Bin 0 -> 25596 bytes
.../admin/fonts/Satoshi-MediumItalic.eot | Bin 0 -> 76888 bytes
.../admin/fonts/Satoshi-MediumItalic.ttf | Bin 0 -> 76696 bytes
.../admin/fonts/Satoshi-MediumItalic.woff | Bin 0 -> 34576 bytes
.../admin/fonts/Satoshi-MediumItalic.woff2 | Bin 0 -> 26696 bytes
src/client/admin/fonts/Satoshi-Regular.eot | Bin 0 -> 73634 bytes
src/client/admin/fonts/Satoshi-Regular.ttf | Bin 0 -> 73476 bytes
src/client/admin/fonts/Satoshi-Regular.woff | Bin 0 -> 33024 bytes
src/client/admin/fonts/Satoshi-Regular.woff2 | Bin 0 -> 25516 bytes
src/client/admin/fonts/Satoshi-Variable.eot | Bin 0 -> 127628 bytes
src/client/admin/fonts/Satoshi-Variable.ttf | Bin 0 -> 127420 bytes
src/client/admin/fonts/Satoshi-Variable.woff | Bin 0 -> 35160 bytes
src/client/admin/fonts/Satoshi-Variable.woff2 | Bin 0 -> 42588 bytes
.../admin/fonts/Satoshi-VariableItalic.eot | Bin 0 -> 129984 bytes
.../admin/fonts/Satoshi-VariableItalic.ttf | Bin 0 -> 129748 bytes
.../admin/fonts/Satoshi-VariableItalic.woff | Bin 0 -> 36472 bytes
.../admin/fonts/Satoshi-VariableItalic.woff2 | Bin 0 -> 43844 bytes
src/client/admin/images/brand/brand-01.svg | 14 +
src/client/admin/images/brand/brand-02.svg | 4 +
src/client/admin/images/brand/brand-03.svg | 11 +
src/client/admin/images/brand/brand-04.svg | 4 +
src/client/admin/images/brand/brand-05.svg | 11 +
src/client/admin/images/cards/cards-01.png | Bin 0 -> 411518 bytes
src/client/admin/images/cards/cards-02.png | Bin 0 -> 467942 bytes
src/client/admin/images/cards/cards-03.png | Bin 0 -> 401625 bytes
src/client/admin/images/cards/cards-04.png | Bin 0 -> 262616 bytes
src/client/admin/images/cards/cards-05.png | Bin 0 -> 411521 bytes
src/client/admin/images/cards/cards-06.png | Bin 0 -> 437504 bytes
.../admin/images/country/country-01.svg | 33 ++
.../admin/images/country/country-02.svg | 18 +
.../admin/images/country/country-03.svg | 17 +
.../admin/images/country/country-04.svg | 17 +
.../admin/images/country/country-05.svg | 34 ++
.../admin/images/country/country-06.svg | 19 +
src/client/admin/images/cover/cover-01.png | Bin 0 -> 482382 bytes
src/client/admin/images/favicon.ico | Bin 0 -> 15406 bytes
.../admin/images/icon/icon-arrow-down.svg | 6 +
.../admin/images/icon/icon-calendar.svg | 6 +
.../admin/images/icon/icon-copy-alt.svg | 6 +
src/client/admin/images/icon/icon-moon.svg | 10 +
src/client/admin/images/icon/icon-sun.svg | 10 +
src/client/admin/images/logo/logo-dark.svg | 53 ++
src/client/admin/images/logo/logo-icon.svg | 44 ++
src/client/admin/images/logo/logo.svg | 53 ++
src/client/admin/images/task/task-01.jpg | Bin 0 -> 77564 bytes
src/client/admin/images/user/user-01.png | Bin 0 -> 15162 bytes
src/client/admin/images/user/user-02.png | Bin 0 -> 20974 bytes
src/client/admin/images/user/user-03.png | Bin 0 -> 20633 bytes
src/client/admin/images/user/user-04.png | Bin 0 -> 23545 bytes
src/client/admin/images/user/user-05.png | Bin 0 -> 19610 bytes
src/client/admin/images/user/user-06.png | Bin 0 -> 86183 bytes
src/client/admin/images/user/user-07.png | Bin 0 -> 10486 bytes
src/client/admin/images/user/user-08.png | Bin 0 -> 11106 bytes
src/client/admin/images/user/user-09.png | Bin 0 -> 9692 bytes
src/client/admin/images/user/user-10.png | Bin 0 -> 9427 bytes
src/client/admin/images/user/user-11.png | Bin 0 -> 11561 bytes
src/client/admin/images/user/user-12.png | Bin 0 -> 14426 bytes
src/client/admin/images/user/user-13.png | Bin 0 -> 7712 bytes
src/client/admin/js/drag.ts | 53 ++
src/client/admin/js/us-aea-en.js | 1 +
src/client/admin/layout/DefaultLayout.tsx | 41 ++
src/client/admin/pages/Calendar.tsx | 274 +++++++++
src/client/admin/pages/Chart.tsx | 23 +
.../admin/pages/Dashboard/ECommerce.tsx | 31 +
src/client/admin/pages/Form/FormElements.tsx | 345 ++++++++++++
src/client/admin/pages/Form/FormLayout.tsx | 258 +++++++++
src/client/admin/pages/Settings.tsx | 325 +++++++++++
src/client/admin/pages/UiElements/Alerts.tsx | 99 ++++
src/client/admin/pages/UiElements/Buttons.tsx | 471 ++++++++++++++++
src/client/admin/pages/Users.tsx | 17 +
src/client/hooks/useColorMode.tsx | 18 +
src/client/hooks/useLocalStorage.tsx | 43 ++
src/client/hooks/useReferrer.tsx | 26 +
src/server/actions.ts | 17 +-
tailwind.config.cjs | 247 +++++++-
134 files changed, 5255 insertions(+), 17 deletions(-)
create mode 100644 migrations/20231113091516_user_last_active_timestamp/migration.sql
create mode 100644 migrations/20231113113101_referrer/migration.sql
create mode 100644 src/client/admin/common/Loader/index.tsx
create mode 100644 src/client/admin/components/BarChart.tsx
create mode 100644 src/client/admin/components/Breadcrumb.tsx
create mode 100644 src/client/admin/components/CheckboxOne.tsx
create mode 100644 src/client/admin/components/CheckboxTwo.tsx
create mode 100644 src/client/admin/components/DailyActiveUsersChart.tsx
create mode 100644 src/client/admin/components/DarkModeSwitcher.tsx
create mode 100644 src/client/admin/components/DataStats.tsx
create mode 100644 src/client/admin/components/DropdownEditDelete.tsx
create mode 100644 src/client/admin/components/DropdownMessage.tsx
create mode 100644 src/client/admin/components/DropdownUser.tsx
create mode 100644 src/client/admin/components/Header.tsx
create mode 100644 src/client/admin/components/PieChart.tsx
create mode 100644 src/client/admin/components/ReferrerTable.tsx
create mode 100644 src/client/admin/components/Sidebar.tsx
create mode 100644 src/client/admin/components/SidebarLinkGroup.tsx
create mode 100644 src/client/admin/components/SwitcherOne.tsx
create mode 100644 src/client/admin/components/SwitcherTwo.tsx
create mode 100644 src/client/admin/components/TotalPaidViewsCard.tsx
create mode 100644 src/client/admin/components/TotalPayingUsersCard.tsx
create mode 100644 src/client/admin/components/TotalProfitCard.tsx
create mode 100644 src/client/admin/components/TotalSignupsCard.tsx
create mode 100644 src/client/admin/components/UsersTable.tsx
create mode 100644 src/client/admin/fonts/Satoshi-Black.eot
create mode 100644 src/client/admin/fonts/Satoshi-Black.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Black.woff
create mode 100644 src/client/admin/fonts/Satoshi-Black.woff2
create mode 100644 src/client/admin/fonts/Satoshi-BlackItalic.eot
create mode 100644 src/client/admin/fonts/Satoshi-BlackItalic.ttf
create mode 100644 src/client/admin/fonts/Satoshi-BlackItalic.woff
create mode 100644 src/client/admin/fonts/Satoshi-BlackItalic.woff2
create mode 100644 src/client/admin/fonts/Satoshi-Bold.eot
create mode 100644 src/client/admin/fonts/Satoshi-Bold.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Bold.woff
create mode 100644 src/client/admin/fonts/Satoshi-Bold.woff2
create mode 100644 src/client/admin/fonts/Satoshi-BoldItalic.eot
create mode 100644 src/client/admin/fonts/Satoshi-BoldItalic.ttf
create mode 100644 src/client/admin/fonts/Satoshi-BoldItalic.woff
create mode 100644 src/client/admin/fonts/Satoshi-BoldItalic.woff2
create mode 100644 src/client/admin/fonts/Satoshi-Italic.eot
create mode 100644 src/client/admin/fonts/Satoshi-Italic.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Italic.woff
create mode 100644 src/client/admin/fonts/Satoshi-Italic.woff2
create mode 100644 src/client/admin/fonts/Satoshi-Light.eot
create mode 100644 src/client/admin/fonts/Satoshi-Light.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Light.woff
create mode 100644 src/client/admin/fonts/Satoshi-Light.woff2
create mode 100644 src/client/admin/fonts/Satoshi-LightItalic.eot
create mode 100644 src/client/admin/fonts/Satoshi-LightItalic.ttf
create mode 100644 src/client/admin/fonts/Satoshi-LightItalic.woff
create mode 100644 src/client/admin/fonts/Satoshi-LightItalic.woff2
create mode 100644 src/client/admin/fonts/Satoshi-Medium.eot
create mode 100644 src/client/admin/fonts/Satoshi-Medium.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Medium.woff
create mode 100644 src/client/admin/fonts/Satoshi-Medium.woff2
create mode 100644 src/client/admin/fonts/Satoshi-MediumItalic.eot
create mode 100644 src/client/admin/fonts/Satoshi-MediumItalic.ttf
create mode 100644 src/client/admin/fonts/Satoshi-MediumItalic.woff
create mode 100644 src/client/admin/fonts/Satoshi-MediumItalic.woff2
create mode 100644 src/client/admin/fonts/Satoshi-Regular.eot
create mode 100644 src/client/admin/fonts/Satoshi-Regular.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Regular.woff
create mode 100644 src/client/admin/fonts/Satoshi-Regular.woff2
create mode 100644 src/client/admin/fonts/Satoshi-Variable.eot
create mode 100644 src/client/admin/fonts/Satoshi-Variable.ttf
create mode 100644 src/client/admin/fonts/Satoshi-Variable.woff
create mode 100644 src/client/admin/fonts/Satoshi-Variable.woff2
create mode 100644 src/client/admin/fonts/Satoshi-VariableItalic.eot
create mode 100644 src/client/admin/fonts/Satoshi-VariableItalic.ttf
create mode 100644 src/client/admin/fonts/Satoshi-VariableItalic.woff
create mode 100644 src/client/admin/fonts/Satoshi-VariableItalic.woff2
create mode 100644 src/client/admin/images/brand/brand-01.svg
create mode 100644 src/client/admin/images/brand/brand-02.svg
create mode 100644 src/client/admin/images/brand/brand-03.svg
create mode 100644 src/client/admin/images/brand/brand-04.svg
create mode 100644 src/client/admin/images/brand/brand-05.svg
create mode 100644 src/client/admin/images/cards/cards-01.png
create mode 100644 src/client/admin/images/cards/cards-02.png
create mode 100644 src/client/admin/images/cards/cards-03.png
create mode 100644 src/client/admin/images/cards/cards-04.png
create mode 100644 src/client/admin/images/cards/cards-05.png
create mode 100644 src/client/admin/images/cards/cards-06.png
create mode 100644 src/client/admin/images/country/country-01.svg
create mode 100644 src/client/admin/images/country/country-02.svg
create mode 100644 src/client/admin/images/country/country-03.svg
create mode 100644 src/client/admin/images/country/country-04.svg
create mode 100644 src/client/admin/images/country/country-05.svg
create mode 100644 src/client/admin/images/country/country-06.svg
create mode 100644 src/client/admin/images/cover/cover-01.png
create mode 100644 src/client/admin/images/favicon.ico
create mode 100644 src/client/admin/images/icon/icon-arrow-down.svg
create mode 100644 src/client/admin/images/icon/icon-calendar.svg
create mode 100644 src/client/admin/images/icon/icon-copy-alt.svg
create mode 100644 src/client/admin/images/icon/icon-moon.svg
create mode 100644 src/client/admin/images/icon/icon-sun.svg
create mode 100644 src/client/admin/images/logo/logo-dark.svg
create mode 100644 src/client/admin/images/logo/logo-icon.svg
create mode 100644 src/client/admin/images/logo/logo.svg
create mode 100644 src/client/admin/images/task/task-01.jpg
create mode 100644 src/client/admin/images/user/user-01.png
create mode 100644 src/client/admin/images/user/user-02.png
create mode 100644 src/client/admin/images/user/user-03.png
create mode 100644 src/client/admin/images/user/user-04.png
create mode 100644 src/client/admin/images/user/user-05.png
create mode 100644 src/client/admin/images/user/user-06.png
create mode 100644 src/client/admin/images/user/user-07.png
create mode 100644 src/client/admin/images/user/user-08.png
create mode 100644 src/client/admin/images/user/user-09.png
create mode 100644 src/client/admin/images/user/user-10.png
create mode 100644 src/client/admin/images/user/user-11.png
create mode 100644 src/client/admin/images/user/user-12.png
create mode 100644 src/client/admin/images/user/user-13.png
create mode 100644 src/client/admin/js/drag.ts
create mode 100644 src/client/admin/js/us-aea-en.js
create mode 100644 src/client/admin/layout/DefaultLayout.tsx
create mode 100644 src/client/admin/pages/Calendar.tsx
create mode 100644 src/client/admin/pages/Chart.tsx
create mode 100644 src/client/admin/pages/Dashboard/ECommerce.tsx
create mode 100644 src/client/admin/pages/Form/FormElements.tsx
create mode 100644 src/client/admin/pages/Form/FormLayout.tsx
create mode 100644 src/client/admin/pages/Settings.tsx
create mode 100644 src/client/admin/pages/UiElements/Alerts.tsx
create mode 100644 src/client/admin/pages/UiElements/Buttons.tsx
create mode 100644 src/client/admin/pages/Users.tsx
create mode 100644 src/client/hooks/useColorMode.tsx
create mode 100644 src/client/hooks/useLocalStorage.tsx
create mode 100644 src/client/hooks/useReferrer.tsx
diff --git a/main.wasp b/main.wasp
index d2fef5b..105deaa 100644
--- a/main.wasp
+++ b/main.wasp
@@ -62,6 +62,11 @@ app SaaSTemplate {
("node-fetch", "3.3.0"),
("react-hook-form", "^7.45.4"),
("stripe", "11.15.0"),
+ ("react-hot-toast", "^2.4.1"),
+ ("react-apexcharts", "^1.4.1"),
+ ("apexcharts", "^3.41.0"),
+ ("headlessui", "^0.0.0"),
+
],
}
@@ -73,9 +78,11 @@ entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
+ lastActiveTimestamp DateTime @default(now())
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
+ referrer String @default("unknown")
stripeId String?
checkoutSessionId String?
hasPaid Boolean @default(false)
@@ -85,6 +92,7 @@ entity User {=psl
credits Int @default(3)
relatedObject RelatedObject[]
externalAuthAssociations SocialLogin[]
+ contactFormMessages ContactFormMessage[]
psl=}
entity SocialLogin {=psl
@@ -107,6 +115,15 @@ entity RelatedObject {=psl
updatedAt DateTime @updatedAt
psl=}
+entity ContactFormMessage {=psl
+ id String @id @default(uuid())
+ content String
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+ createdAt DateTime @default(now())
+ isRead Boolean @default(false)
+ repliedAt DateTime?
+psl=}
/* 📡 These are the Wasp Routes (You can protect them easily w/ 'authRequired: true');
* https://wasp-lang.dev/docs/tutorial/pages
@@ -169,6 +186,60 @@ page CheckoutPage {
component: import Checkout from "@client/CheckoutPage"
}
+route AdminRoute { path: "/admin", to: DashboardPage }
+page DashboardPage {
+ authRequired: true,
+ component: import Dashboard from "@client/admin/pages/Dashboard/Ecommerce"
+}
+
+route AdminUsersRoute { path: "/admin/users", to: AdminUsersPage }
+page AdminUsersPage {
+ authRequired: true,
+ component: import AdminUsers from "@client/admin/pages/Users"
+}
+
+route AdminSettingsRoute { path: "/admin/settings", to: AdminSettingsPage }
+page AdminSettingsPage {
+ authRequired: true,
+ component: import AdminSettings from "@client/admin/pages/Settings"
+}
+
+route AdminChartsRoute { path: "/admin/chart", to: AdminChartsPage }
+page AdminChartsPage {
+ authRequired: true,
+ component: import AdminCharts from "@client/admin/pages/Chart"
+}
+
+route AdminFormElementsRoute { path: "/admin/forms/form-elements", to: AdminFormElementsPage }
+page AdminFormElementsPage {
+ authRequired: true,
+ component: import AdminForms from "@client/admin/pages/Form/FormElements"
+}
+
+route AdminFormLayoutsRoute { path: "/admin/forms/form-layouts", to: AdminFormLayoutsPage }
+page AdminFormLayoutsPage {
+ authRequired: true,
+ component: import AdminForms from "@client/admin/pages/Form/FormLayout"
+}
+
+route AdminCalendarRoute { path: "/admin/calendar", to: AdminCalendarPage }
+page AdminCalendarPage {
+ authRequired: true,
+ component: import AdminCalendar from "@client/admin/pages/Calendar"
+}
+
+route AdminUIAlertsRoute { path: "/admin/ui/alerts", to: AdminUIAlertsPage }
+page AdminUIAlertsPage {
+ authRequired: true,
+ component: import AdminUI from "@client/admin/pages/UiElements/Alerts"
+}
+
+route AdminUIButtonsRoute { path: "/admin/ui/buttons", to: AdminUIButtonsPage }
+page AdminUIButtonsPage {
+ authRequired: true,
+ component: import AdminUI from "@client/admin/pages/UiElements/Buttons"
+}
+
/* ⛑ These are the Wasp Operations, which allow the client and server to interact:
* https://wasp-lang.dev/docs/data-model/operations/overview
*/
@@ -190,10 +261,10 @@ action stripePayment {
// entities: [User]
// }
-// action updateUser {
-// fn: import { updateUser } from "@server/actions.js",
-// entities: [User]
-// }
+action updateUser {
+ fn: import { updateUser } from "@server/actions.js",
+ entities: [User]
+}
// 📚 Queries
diff --git a/migrations/20231113091516_user_last_active_timestamp/migration.sql b/migrations/20231113091516_user_last_active_timestamp/migration.sql
new file mode 100644
index 0000000..71f77fe
--- /dev/null
+++ b/migrations/20231113091516_user_last_active_timestamp/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "lastActiveTimestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "referrer" TEXT NOT NULL DEFAULT 'unknown';
diff --git a/migrations/20231113113101_referrer/migration.sql b/migrations/20231113113101_referrer/migration.sql
new file mode 100644
index 0000000..eac5745
--- /dev/null
+++ b/migrations/20231113113101_referrer/migration.sql
@@ -0,0 +1,14 @@
+-- CreateTable
+CREATE TABLE "ContactFormMessage" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isRead" BOOLEAN NOT NULL DEFAULT false,
+ "repliedAt" TIMESTAMP(3),
+
+ CONSTRAINT "ContactFormMessage_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "ContactFormMessage" ADD CONSTRAINT "ContactFormMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/src/client/App.tsx b/src/client/App.tsx
index d50db01..7a54250 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -1,23 +1,54 @@
import './Main.css';
import NavBar from './NavBar';
-import { useMemo, ReactNode } from 'react';
+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';
+/**
+ * use this component to wrap all child components
+ * this is useful for templates, themes, and context
+ */
export default function App({ children }: { children: ReactNode }) {
const location = useLocation();
+ const { data: user } = useAuth();
+ const [referrer, setReferrer] = useReferrer();
const shouldDisplayAppNavBar = useMemo(() => {
return !location.pathname.startsWith('/landing-page');
}, [location]);
- /**
- * use this component to wrap all child components
- * this is useful for templates, themes, and context
- * in this case the NavBar will always be rendered
- */
+
+ const isAdminDashboard = useMemo(() => {
+ return location.pathname.startsWith('/admin');
+ }, [location]);
+
+ useEffect(() => {
+ if (user) {
+ const lastSeenAt = new Date(user.lastActiveTimestamp);
+ const today = new Date();
+ if (lastSeenAt.getDate() === today.getDate()) return;
+ updateUser({ lastActiveTimestamp: today });
+ }
+ }, [user]);
+
+ useEffect(() => {
+ if (user && referrer && referrer !== UNKOWN_REFERRER) {
+ updateUser({ referrer });
+ setReferrer(null);
+ }
+ }, [user, referrer]);
+
return (
-
- {shouldDisplayAppNavBar &&
}
-
{children}
-
+ <>
+ {isAdminDashboard ? (
+ <>{children}>
+ ) : (
+ <>
+ {shouldDisplayAppNavBar && }
+ {children}
+ >
+ )}
+ >
);
}
diff --git a/src/client/Main.css b/src/client/Main.css
index a90f074..ee73800 100644
--- a/src/client/Main.css
+++ b/src/client/Main.css
@@ -2,3 +2,155 @@
@tailwind components;
@tailwind utilities;
+@layer utilities {
+ /* Chrome, Safari and Opera */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+
+ .no-scrollbar {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+ }
+
+ .chat-height {
+ @apply h-[calc(100vh_-_8.125rem)] lg:h-[calc(100vh_-_5.625rem)];
+ }
+ .inbox-height {
+ @apply h-[calc(100vh_-_8.125rem)] lg:h-[calc(100vh_-_5.625rem)];
+ }
+}
+
+
+
+/* third-party libraries CSS */
+
+.tableCheckbox:checked ~ div span {
+ @apply opacity-100;
+}
+.tableCheckbox:checked ~ div {
+ @apply bg-primary border-primary;
+}
+
+.apexcharts-legend-text {
+ @apply !text-body dark:!text-bodydark;
+}
+.apexcharts-text {
+ @apply !fill-body dark:!fill-bodydark;
+}
+.apexcharts-xcrosshairs {
+ @apply !fill-stroke dark:!fill-strokedark;
+}
+.apexcharts-gridline {
+ @apply !stroke-stroke dark:!stroke-strokedark;
+}
+.apexcharts-series.apexcharts-pie-series path {
+ @apply dark:!stroke-transparent;
+}
+.apexcharts-legend-series {
+ @apply !inline-flex gap-1.5;
+}
+.apexcharts-tooltip.apexcharts-theme-light {
+ @apply dark:!bg-boxdark dark:!border-strokedark;
+}
+.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
+ @apply dark:!bg-meta-4 dark:!border-strokedark;
+}
+.apexcharts-xaxistooltip, .apexcharts-yaxistooltip {
+ @apply dark:!bg-meta-4 dark:!border-meta-4 dark:!text-bodydark1;
+}
+.apexcharts-xaxistooltip-bottom:after {
+ @apply dark:!border-b-meta-4;
+}
+.apexcharts-xaxistooltip-bottom:before {
+ @apply dark:!border-b-meta-4;
+
+}
+
+.flatpickr-day.selected {
+ @apply bg-primary border-primary hover:bg-primary hover:border-primary;
+}
+.flatpickr-months .flatpickr-prev-month:hover svg,
+.flatpickr-months .flatpickr-next-month:hover svg {
+ @apply fill-primary;
+}
+.flatpickr-calendar.arrowTop:before {
+ @apply dark:!border-b-boxdark;
+}
+.flatpickr-calendar.arrowTop:after {
+ @apply dark:!border-b-boxdark;
+}
+.flatpickr-calendar {
+ @apply dark:!bg-boxdark dark:!text-bodydark dark:!shadow-8 !p-6 2xsm:!w-auto;
+}
+.flatpickr-day {
+ @apply dark:!text-bodydark;
+}
+.flatpickr-months .flatpickr-prev-month, .flatpickr-months .flatpickr-next-month {
+ @apply !top-7 dark:!text-white dark:!fill-white;
+}
+.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month, .flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
+ @apply !left-7
+}
+.flatpickr-months .flatpickr-prev-month.flatpickr-next-month, .flatpickr-months .flatpickr-next-month.flatpickr-next-month {
+ @apply !right-7
+}
+span.flatpickr-weekday,
+.flatpickr-months .flatpickr-month {
+ @apply dark:!text-white dark:!fill-white;
+}
+.flatpickr-day.inRange {
+ @apply dark:!bg-meta-4 dark:!border-meta-4 dark:!shadow-7;
+}
+.flatpickr-day.selected, .flatpickr-day.startRange,
+.flatpickr-day.selected, .flatpickr-day.endRange {
+ @apply dark:!text-white;
+}
+
+.map-btn .jvm-zoom-btn {
+ @apply flex items-center justify-center w-7.5 h-7.5 rounded border border-stroke dark:border-strokedark hover:border-primary dark:hover:border-primary bg-white hover:bg-primary text-body hover:text-white dark:text-bodydark dark:hover:text-white text-2xl leading-none px-0 pt-0 pb-0.5;
+}
+.mapOne .jvm-zoom-btn {
+ @apply left-auto top-auto bottom-0;
+}
+.mapOne .jvm-zoom-btn.jvm-zoomin {
+ @apply right-10;
+}
+.mapOne .jvm-zoom-btn.jvm-zoomout {
+ @apply right-0;
+}
+.mapTwo .jvm-zoom-btn {
+ @apply top-auto bottom-0;
+}
+.mapTwo .jvm-zoom-btn.jvm-zoomin {
+ @apply left-0;
+}
+.mapTwo .jvm-zoom-btn.jvm-zoomout {
+ @apply left-10;
+}
+
+.taskCheckbox:checked ~ .box span {
+ @apply opacity-100;
+}
+.taskCheckbox:checked ~ p {
+ @apply line-through;
+}
+.taskCheckbox:checked ~ .box {
+ @apply bg-primary border-primary dark:border-primary;
+}
+
+.custom-input-date::-webkit-calendar-picker-indicator {
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 20px;
+}
+.custom-input-date-1::-webkit-calendar-picker-indicator {
+ background-image: url(./images/icon/icon-calendar.svg);
+}
+.custom-input-date-2::-webkit-calendar-picker-indicator {
+ background-image: url(./images/icon/icon-arrow-down.svg);
+}
+
+[x-cloak] {
+ display: none !important;
+}
\ No newline at end of file
diff --git a/src/client/admin/common/Loader/index.tsx b/src/client/admin/common/Loader/index.tsx
new file mode 100644
index 0000000..0915c82
--- /dev/null
+++ b/src/client/admin/common/Loader/index.tsx
@@ -0,0 +1,9 @@
+const Loader = () => {
+ return (
+
+ );
+};
+
+export default Loader;
diff --git a/src/client/admin/components/BarChart.tsx b/src/client/admin/components/BarChart.tsx
new file mode 100644
index 0000000..242e96e
--- /dev/null
+++ b/src/client/admin/components/BarChart.tsx
@@ -0,0 +1,146 @@
+import { ApexOptions } from 'apexcharts';
+import React, { useState } from 'react';
+import ReactApexChart from 'react-apexcharts';
+
+interface BarChartState {
+ series: { data: number[] }[];
+}
+
+const BarChart: React.FC = () => {
+ const [state, setState] = useState({
+ series: [
+ {
+ data: [
+ 168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112, 123, 212,
+ 270, 190, 310, 115, 90, 380, 112, 223, 292, 170, 290, 110, 115, 290,
+ 380, 312,
+ ],
+ },
+ ],
+ });
+
+ const options: ApexOptions = {
+ colors: ['#3C50E0'],
+ chart: {
+ fontFamily: 'Satoshi, sans-serif',
+ type: 'bar',
+ height: 350,
+ toolbar: {
+ show: false,
+ },
+ },
+ plotOptions: {
+ bar: {
+ horizontal: false,
+ columnWidth: '55%',
+ // endingShape: "rounded",
+ borderRadius: 2,
+ },
+ },
+ dataLabels: {
+ enabled: false,
+ },
+ stroke: {
+ show: true,
+ width: 4,
+ colors: ['transparent'],
+ },
+ xaxis: {
+ categories: [
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ '12',
+ '13',
+ '14',
+ '15',
+ '16',
+ '17',
+ '18',
+ '19',
+ '20',
+ '21',
+ '22',
+ '23',
+ '24',
+ '25',
+ '26',
+ '27',
+ '28',
+ '29',
+ '30',
+ ],
+ axisBorder: {
+ show: false,
+ },
+ axisTicks: {
+ show: false,
+ },
+ },
+ legend: {
+ show: true,
+ position: 'top',
+ horizontalAlign: 'left',
+ fontFamily: 'inter',
+
+ markers: {
+ radius: 99,
+ },
+ },
+ // yaxis: {
+ // title: false,
+ // },
+ grid: {
+ yaxis: {
+ lines: {
+ show: false,
+ },
+ },
+ },
+ fill: {
+ opacity: 1,
+ },
+
+ tooltip: {
+ x: {
+ show: false,
+ },
+ // y: {
+ // formatter: function (val) {
+ // return val;
+ // },
+ // },
+ },
+ };
+
+ return (
+
+
+
+ Visitors Analytics
+
+
+
+
+
+ );
+};
+
+export default BarChart;
diff --git a/src/client/admin/components/Breadcrumb.tsx b/src/client/admin/components/Breadcrumb.tsx
new file mode 100644
index 0000000..3e3a613
--- /dev/null
+++ b/src/client/admin/components/Breadcrumb.tsx
@@ -0,0 +1,24 @@
+import { Link } from 'react-router-dom';
+interface BreadcrumbProps {
+ pageName: string;
+}
+const Breadcrumb = ({ pageName }: BreadcrumbProps) => {
+ return (
+
+
+ {pageName}
+
+
+
+
+ );
+};
+
+export default Breadcrumb;
diff --git a/src/client/admin/components/CheckboxOne.tsx b/src/client/admin/components/CheckboxOne.tsx
new file mode 100644
index 0000000..138d413
--- /dev/null
+++ b/src/client/admin/components/CheckboxOne.tsx
@@ -0,0 +1,41 @@
+import { useState } from 'react';
+
+const CheckboxOne = () => {
+ const [isChecked, setIsChecked] = useState(false);
+
+ return (
+
+
+
+ );
+};
+
+export default CheckboxOne;
diff --git a/src/client/admin/components/CheckboxTwo.tsx b/src/client/admin/components/CheckboxTwo.tsx
new file mode 100644
index 0000000..c9988b3
--- /dev/null
+++ b/src/client/admin/components/CheckboxTwo.tsx
@@ -0,0 +1,50 @@
+import { useState } from 'react';
+
+const CheckboxTwo = () => {
+ const [isChecked, setIsChecked] = useState(false);
+
+ return (
+
+
+
+ );
+};
+
+export default CheckboxTwo;
diff --git a/src/client/admin/components/DailyActiveUsersChart.tsx b/src/client/admin/components/DailyActiveUsersChart.tsx
new file mode 100644
index 0000000..93085ea
--- /dev/null
+++ b/src/client/admin/components/DailyActiveUsersChart.tsx
@@ -0,0 +1,192 @@
+import { ApexOptions } from 'apexcharts';
+import React, { useState } from 'react';
+import ReactApexChart from 'react-apexcharts';
+
+const options: ApexOptions = {
+ legend: {
+ show: false,
+ position: 'top',
+ horizontalAlign: 'left',
+ },
+ colors: ['#3C50E0', '#80CAEE'],
+ chart: {
+ fontFamily: 'Satoshi, sans-serif',
+ height: 335,
+ type: 'area',
+ dropShadow: {
+ enabled: true,
+ color: '#623CEA14',
+ top: 10,
+ blur: 4,
+ left: 0,
+ opacity: 0.1,
+ },
+
+ toolbar: {
+ show: false,
+ },
+ },
+ responsive: [
+ {
+ breakpoint: 1024,
+ options: {
+ chart: {
+ height: 300,
+ },
+ },
+ },
+ {
+ breakpoint: 1366,
+ options: {
+ chart: {
+ height: 350,
+ },
+ },
+ },
+ ],
+ stroke: {
+ width: [2, 2],
+ curve: 'straight',
+ },
+ // labels: {
+ // show: false,
+ // position: "top",
+ // },
+ grid: {
+ xaxis: {
+ lines: {
+ show: true,
+ },
+ },
+ yaxis: {
+ lines: {
+ show: true,
+ },
+ },
+ },
+ dataLabels: {
+ enabled: false,
+ },
+ markers: {
+ size: 4,
+ colors: '#fff',
+ strokeColors: ['#3056D3', '#80CAEE'],
+ strokeWidth: 3,
+ strokeOpacity: 0.9,
+ strokeDashArray: 0,
+ fillOpacity: 1,
+ discrete: [],
+ hover: {
+ size: undefined,
+ sizeOffset: 5,
+ },
+ },
+ xaxis: {
+ type: 'category',
+ categories: [
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ ],
+ axisBorder: {
+ show: false,
+ },
+ axisTicks: {
+ show: false,
+ },
+ },
+ yaxis: {
+ title: {
+ style: {
+ fontSize: '0px',
+ },
+ },
+ min: 0,
+ max: 100,
+ },
+};
+
+interface ChartOneState {
+ series: {
+ name: string;
+ data: number[];
+ }[];
+}
+
+const DailyActiveUsersChart: React.FC = () => {
+ const [state, setState] = useState({
+ series: [
+ {
+ name: 'Product One',
+ data: [23, 0],
+ },
+
+ {
+ name: 'Product Two',
+ data: [30, 25, 36, 30, 45, 35, 64, 52, 59, 36, 39, 51],
+ },
+ ],
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
Total Revenue
+
12.04.2022 - 12.05.2022
+
+
+
+
+
+
+
+
Total Sales
+
12.04.2022 - 12.05.2022
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DailyActiveUsersChart;
diff --git a/src/client/admin/components/DarkModeSwitcher.tsx b/src/client/admin/components/DarkModeSwitcher.tsx
new file mode 100644
index 0000000..3918fd3
--- /dev/null
+++ b/src/client/admin/components/DarkModeSwitcher.tsx
@@ -0,0 +1,65 @@
+import useColorMode from '../../hooks/useColorMode';
+
+const DarkModeSwitcher = () => {
+ const [colorMode, setColorMode] = useColorMode();
+
+ return (
+
+
+
+ );
+};
+
+export default DarkModeSwitcher;
diff --git a/src/client/admin/components/DataStats.tsx b/src/client/admin/components/DataStats.tsx
new file mode 100644
index 0000000..73910ad
--- /dev/null
+++ b/src/client/admin/components/DataStats.tsx
@@ -0,0 +1,102 @@
+const DataStats = () => {
+ return (
+
+
+
+
+
+ $4,350
+
+
Unique Visitors
+
+
+
+
+
+
+ 55.9K
+
+
Total Pageviews
+
+
+
+
+
+
+ 54%
+
+
Bounce Rate
+
+
+
+
+
+
+ 2m 56s
+
+
Visit Duration
+
+
+
+
+
+ );
+};
+
+export default DataStats;
diff --git a/src/client/admin/components/DropdownEditDelete.tsx b/src/client/admin/components/DropdownEditDelete.tsx
new file mode 100644
index 0000000..d31fe8a
--- /dev/null
+++ b/src/client/admin/components/DropdownEditDelete.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useRef, useState } from "react";
+
+const DropdownDefault = () => {
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+
+ const trigger = useRef(null);
+ const dropdown = useRef(null);
+
+ // close on click outside
+ useEffect(() => {
+ const clickHandler = ({ target }: MouseEvent) => {
+ if (!dropdown.current) return;
+ if (
+ !dropdownOpen ||
+ dropdown.current.contains(target) ||
+ trigger.current.contains(target)
+ )
+ return;
+ setDropdownOpen(false);
+ };
+ document.addEventListener("click", clickHandler);
+ return () => document.removeEventListener("click", clickHandler);
+ });
+
+ // close if the esc key is pressed
+ useEffect(() => {
+ const keyHandler = ({ keyCode }: KeyboardEvent) => {
+ if (!dropdownOpen || keyCode !== 27) return;
+ setDropdownOpen(false);
+ };
+ document.addEventListener("keydown", keyHandler);
+ return () => document.removeEventListener("keydown", keyHandler);
+ });
+
+ return (
+
+
+
setDropdownOpen(true)}
+ onBlur={() => setDropdownOpen(false)}
+ className={`absolute right-0 top-full z-40 w-40 space-y-1 rounded-sm border border-stroke bg-white p-1.5 shadow-default dark:border-strokedark dark:bg-boxdark ${
+ dropdownOpen === true ? "block" : "hidden"
+ }`}
+ >
+
+
+
+
+ );
+};
+
+export default DropdownDefault;
diff --git a/src/client/admin/components/DropdownMessage.tsx b/src/client/admin/components/DropdownMessage.tsx
new file mode 100644
index 0000000..ceee6e6
--- /dev/null
+++ b/src/client/admin/components/DropdownMessage.tsx
@@ -0,0 +1,48 @@
+// import { useEffect, useRef, useState } from 'react';
+import { Link } from 'react-router-dom';
+
+const DropdownMessage = () => {
+
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DropdownMessage;
diff --git a/src/client/admin/components/DropdownUser.tsx b/src/client/admin/components/DropdownUser.tsx
new file mode 100644
index 0000000..4c022f5
--- /dev/null
+++ b/src/client/admin/components/DropdownUser.tsx
@@ -0,0 +1,138 @@
+import { useEffect, useRef, useState } from 'react';
+import { Link } from 'react-router-dom';
+
+import UserOne from '../images/user/user-01.png';
+
+const DropdownUser = () => {
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+
+ const trigger = useRef(null);
+ const dropdown = useRef(null);
+
+ // close on click outside
+ useEffect(() => {
+ const clickHandler = ({ target }: MouseEvent) => {
+ if (!dropdown.current) return;
+ if (
+ !dropdownOpen ||
+ dropdown.current.contains(target) ||
+ trigger.current.contains(target)
+ )
+ return;
+ setDropdownOpen(false);
+ };
+ document.addEventListener('click', clickHandler);
+ return () => document.removeEventListener('click', clickHandler);
+ });
+
+ // close if the esc key is pressed
+ useEffect(() => {
+ const keyHandler = ({ keyCode }: KeyboardEvent) => {
+ if (!dropdownOpen || keyCode !== 27) return;
+ setDropdownOpen(false);
+ };
+ document.addEventListener('keydown', keyHandler);
+ return () => document.removeEventListener('keydown', keyHandler);
+ });
+
+ return (
+
+
setDropdownOpen(!dropdownOpen)}
+ className="flex items-center gap-4"
+ to="#"
+ >
+
+
+ Thomas Anree
+
+ UX Designer
+
+
+
+
+
+
+
+
+
+ {/* */}
+
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'
+ }`}
+ >
+
+ -
+
+
+ Account Settings
+
+
+
+
+
+ {/* */}
+
+ );
+};
+
+export default DropdownUser;
diff --git a/src/client/admin/components/Header.tsx b/src/client/admin/components/Header.tsx
new file mode 100644
index 0000000..0f8b297
--- /dev/null
+++ b/src/client/admin/components/Header.tsx
@@ -0,0 +1,119 @@
+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 './DropdownUser';
+
+const Header = (props: {
+ sidebarOpen: string | boolean | undefined;
+ setSidebarOpen: (arg0: boolean) => void;
+}) => {
+ return (
+
+
+
+ {/* */}
+
+ {/* */}
+
+
+

+
+
+
+
+
+
+
+ {/* */}
+
+ {/* */}
+
+ {/* */}
+
+ {/* */}
+
+
+ {/* */}
+
+ {/* */}
+
+
+
+ );
+};
+
+export default Header;
diff --git a/src/client/admin/components/PieChart.tsx b/src/client/admin/components/PieChart.tsx
new file mode 100644
index 0000000..4a7042e
--- /dev/null
+++ b/src/client/admin/components/PieChart.tsx
@@ -0,0 +1,150 @@
+import { ApexOptions } from 'apexcharts';
+import React, { useState } from 'react';
+import ReactApexChart from 'react-apexcharts';
+
+interface PieChartState {
+ series: number[];
+}
+
+const options: ApexOptions = {
+ chart: {
+ type: 'donut',
+ },
+ colors: ['#10B981', '#375E83', '#259AE6', '#FFA70B'],
+ labels: ['Remote', 'Hybrid', 'Onsite', 'Leave'],
+ legend: {
+ show: true,
+ position: 'bottom',
+ },
+
+ plotOptions: {
+ pie: {
+ donut: {
+ size: '65%',
+ background: 'transparent',
+ },
+ },
+ },
+ dataLabels: {
+ enabled: false,
+ },
+ responsive: [
+ {
+ breakpoint: 2600,
+ options: {
+ chart: {
+ width: 380,
+ },
+ },
+ },
+ {
+ breakpoint: 640,
+ options: {
+ chart: {
+ width: 200,
+ },
+ },
+ },
+ ],
+};
+
+const PieChart: React.FC = () => {
+ const [state, setState] = useState({
+ series: [65, 34, 12, 56],
+ });
+
+ return (
+
+
+
+
+ Visitors Analytics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PieChart;
diff --git a/src/client/admin/components/ReferrerTable.tsx b/src/client/admin/components/ReferrerTable.tsx
new file mode 100644
index 0000000..cd8009b
--- /dev/null
+++ b/src/client/admin/components/ReferrerTable.tsx
@@ -0,0 +1,176 @@
+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';
+
+const ReferrerTable = () => {
+ return (
+
+
+ Top Channels
+
+
+
+
+
+
+ Source
+
+
+
+
+ Visitors
+
+
+
+
+ Revenues
+
+
+
+
+ Sales
+
+
+
+
+ Conversion
+
+
+
+
+
+
+
+

+
+
Google
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+ Twitter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
Github
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
Vimeo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+ Facebook
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ReferrerTable;
diff --git a/src/client/admin/components/Sidebar.tsx b/src/client/admin/components/Sidebar.tsx
new file mode 100644
index 0000000..ca28b0d
--- /dev/null
+++ b/src/client/admin/components/Sidebar.tsx
@@ -0,0 +1,529 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { NavLink, useLocation } from 'react-router-dom';
+import Logo from '../../static/logo.png';
+import SidebarLinkGroup from './SidebarLinkGroup';
+
+interface SidebarProps {
+ sidebarOpen: boolean;
+ setSidebarOpen: (arg: boolean) => void;
+}
+
+const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => {
+ const location = useLocation();
+ const { pathname } = location;
+
+ const trigger = useRef(null);
+ const sidebar = useRef(null);
+
+ const storedSidebarExpanded = localStorage.getItem('sidebar-expanded');
+ const [sidebarExpanded, setSidebarExpanded] = useState(
+ storedSidebarExpanded === null ? false : storedSidebarExpanded === 'true',
+ );
+
+ // close on click outside
+ useEffect(() => {
+ const clickHandler = ({ target }: MouseEvent) => {
+ if (!sidebar.current || !trigger.current) return;
+ if (
+ !sidebarOpen ||
+ sidebar.current.contains(target) ||
+ trigger.current.contains(target)
+ )
+ return;
+ setSidebarOpen(false);
+ };
+ document.addEventListener('click', clickHandler);
+ return () => document.removeEventListener('click', clickHandler);
+ });
+
+ // close if the esc key is pressed
+ useEffect(() => {
+ const keyHandler = ({ keyCode }: KeyboardEvent) => {
+ if (!sidebarOpen || keyCode !== 27) return;
+ setSidebarOpen(false);
+ };
+ document.addEventListener('keydown', keyHandler);
+ return () => document.removeEventListener('keydown', keyHandler);
+ });
+
+ useEffect(() => {
+ localStorage.setItem('sidebar-expanded', sidebarExpanded.toString());
+ if (sidebarExpanded) {
+ document.querySelector('body')?.classList.add('sidebar-expanded');
+ } else {
+ document.querySelector('body')?.classList.remove('sidebar-expanded');
+ }
+ }, [sidebarExpanded]);
+
+ return (
+
+ );
+};
+
+export default Sidebar;
diff --git a/src/client/admin/components/SidebarLinkGroup.tsx b/src/client/admin/components/SidebarLinkGroup.tsx
new file mode 100644
index 0000000..5330b12
--- /dev/null
+++ b/src/client/admin/components/SidebarLinkGroup.tsx
@@ -0,0 +1,21 @@
+import { ReactNode, useState } from 'react';
+
+interface SidebarLinkGroupProps {
+ children: (handleClick: () => void, open: boolean) => ReactNode;
+ activeCondition: boolean;
+}
+
+const SidebarLinkGroup = ({
+ children,
+ activeCondition,
+}: SidebarLinkGroupProps) => {
+ const [open, setOpen] = useState(activeCondition);
+
+ const handleClick = () => {
+ setOpen(!open);
+ };
+
+ return {children(handleClick, open)};
+};
+
+export default SidebarLinkGroup;
diff --git a/src/client/admin/components/SwitcherOne.tsx b/src/client/admin/components/SwitcherOne.tsx
new file mode 100644
index 0000000..11125f6
--- /dev/null
+++ b/src/client/admin/components/SwitcherOne.tsx
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+
+const SwitcherOne = () => {
+ const [enabled, setEnabled] = useState(false);
+
+ return (
+
+
+
+ );
+};
+
+export default SwitcherOne;
diff --git a/src/client/admin/components/SwitcherTwo.tsx b/src/client/admin/components/SwitcherTwo.tsx
new file mode 100644
index 0000000..9ede1db
--- /dev/null
+++ b/src/client/admin/components/SwitcherTwo.tsx
@@ -0,0 +1,66 @@
+import { useState } from 'react';
+
+const SwitcherTwo = () => {
+ const [enabled, setEnabled] = useState(false);
+
+ return (
+
+
+
+ );
+};
+
+export default SwitcherTwo;
diff --git a/src/client/admin/components/TotalPaidViewsCard.tsx b/src/client/admin/components/TotalPaidViewsCard.tsx
new file mode 100644
index 0000000..eb7cd7c
--- /dev/null
+++ b/src/client/admin/components/TotalPaidViewsCard.tsx
@@ -0,0 +1,53 @@
+const TotalPageViewsCard = () => {
+ return (
+
+
+
+
+
+
+ $3.456K
+
+ Total views
+
+
+
+ 0.43%
+
+
+
+
+ );
+};
+
+export default TotalPageViewsCard;
diff --git a/src/client/admin/components/TotalPayingUsersCard.tsx b/src/client/admin/components/TotalPayingUsersCard.tsx
new file mode 100644
index 0000000..082e735
--- /dev/null
+++ b/src/client/admin/components/TotalPayingUsersCard.tsx
@@ -0,0 +1,53 @@
+const TotalPayingUsersCard = () => {
+ return (
+
+
+
+
+
+
+ 2.450
+
+ Total Product
+
+
+
+ 2.59%
+
+
+
+
+ );
+};
+
+export default TotalPayingUsersCard;
diff --git a/src/client/admin/components/TotalProfitCard.tsx b/src/client/admin/components/TotalProfitCard.tsx
new file mode 100644
index 0000000..337dd62
--- /dev/null
+++ b/src/client/admin/components/TotalProfitCard.tsx
@@ -0,0 +1,57 @@
+const TotalProfitCard = () => {
+ return (
+
+
+
+
+
+
+ $45,2K
+
+ Total Profit
+
+
+
+ 4.35%
+
+
+
+
+ );
+};
+
+export default TotalProfitCard;
diff --git a/src/client/admin/components/TotalSignupsCard.tsx b/src/client/admin/components/TotalSignupsCard.tsx
new file mode 100644
index 0000000..d60191b
--- /dev/null
+++ b/src/client/admin/components/TotalSignupsCard.tsx
@@ -0,0 +1,57 @@
+const TotalSignupsCard = () => {
+ return (
+
+
+
+
+
+
+ 3.456
+
+ Total Users
+
+
+
+ 0.95%
+
+
+
+
+ );
+};
+
+export default TotalSignupsCard;
diff --git a/src/client/admin/components/UsersTable.tsx b/src/client/admin/components/UsersTable.tsx
new file mode 100644
index 0000000..42f5209
--- /dev/null
+++ b/src/client/admin/components/UsersTable.tsx
@@ -0,0 +1,62 @@
+import SwitcherOne from "./SwitcherOne";
+import DropdownEditDelete from "./DropdownEditDelete";
+
+const UsersTable = () => {
+ return (
+
+
+
+ Users
+
+
+
+
+
+
+
+
+
+ Apple Watch Series 7
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UsersTable;
diff --git a/src/client/admin/fonts/Satoshi-Black.eot b/src/client/admin/fonts/Satoshi-Black.eot
new file mode 100644
index 0000000000000000000000000000000000000000..11747f362ab2511dd102ce6f794ab9d566adbbef
GIT binary patch
literal 73352
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