From 7addd0ef2314fe8bf293fb537a21087a99af4092 Mon Sep 17 00:00:00 2001 From: yudho Date: Mon, 30 Sep 2024 19:06:06 +0530 Subject: [PATCH] feat(app): add user dashboard and login --- apps/app/package.json | 9 + apps/app/src/components/ActiveLink.tsx | 13 +- apps/app/src/components/ApiUsage/Chart.tsx | 245 ++++++++++ apps/app/src/components/ApiUsage/Stats.tsx | 179 +++++++ .../Campaign/Advertiser/ConfirmModal.tsx | 90 ++++ .../Campaign/Advertiser/Listing.tsx | 279 +++++++++++ .../components/Campaign/Advertiser/Stats.tsx | 220 +++++++++ .../src/components/Campaign/BannerAdForm.tsx | 351 ++++++++++++++ apps/app/src/components/Campaign/Plans.tsx | 216 +++++++++ .../app/src/components/Campaign/PreviewAd.tsx | 70 +++ .../Campaign/Publisher/ConfirmModal.tsx | 74 +++ .../components/Campaign/Publisher/Listing.tsx | 284 +++++++++++ .../components/Campaign/Publisher/Stats.tsx | 186 +++++++ .../app/src/components/Campaign/StartForm.tsx | 182 +++++++ .../src/components/Campaign/TextAdForm.tsx | 379 +++++++++++++++ .../app/src/components/CampaignPagination.tsx | 96 ++++ apps/app/src/components/Chart.tsx | 244 ++++++++++ apps/app/src/components/Dashboard/Addkeys.tsx | 146 ++++++ .../components/Dashboard/DeleteAccount.tsx | 161 +++++++ .../src/components/Dashboard/DeleteKey.tsx | 97 ++++ .../src/components/Dashboard/Pagination.tsx | 165 +++++++ .../src/components/Dashboard/SetPassword.tsx | 127 +++++ .../src/components/Dashboard/UpdateEmail.tsx | 208 ++++++++ .../components/Dashboard/UpdatePassword.tsx | 205 ++++++++ apps/app/src/components/Icons/Avatar.tsx | 32 ++ apps/app/src/components/Icons/Campaign.tsx | 25 + apps/app/src/components/Icons/CircleTimer.tsx | 22 + apps/app/src/components/Icons/Close.tsx | 23 + apps/app/src/components/Icons/Create.tsx | 22 + apps/app/src/components/Icons/Delete.tsx | 22 + apps/app/src/components/Icons/Edit.tsx | 23 + apps/app/src/components/Icons/EmailCircle.tsx | 24 + .../src/components/Icons/FaChevronLeft.tsx | 8 +- .../src/components/Icons/FaChevronRight.tsx | 8 +- apps/app/src/components/Icons/FaDownload.tsx | 21 + .../components/Icons/FaFileInvoiceDollar.tsx | 21 + apps/app/src/components/Icons/FaUserAlt .tsx | 20 + apps/app/src/components/Icons/FileSearch.tsx | 24 + apps/app/src/components/Icons/Home.tsx | 24 + apps/app/src/components/Icons/Key.tsx | 29 ++ apps/app/src/components/Icons/LockCircle.tsx | 24 + apps/app/src/components/Icons/LoginCircle.tsx | 23 + apps/app/src/components/Icons/Logout.tsx | 21 + apps/app/src/components/Icons/Plan.tsx | 25 + apps/app/src/components/Icons/Refresh.tsx | 31 ++ .../app/src/components/Icons/UnlockCircle.tsx | 23 + apps/app/src/components/Icons/User.tsx | 21 +- apps/app/src/components/Icons/Visibility.tsx | 19 + .../src/components/Icons/VisibilityOff.tsx | 19 + apps/app/src/components/Layouts/Header.tsx | 149 ++---- apps/app/src/components/Layouts/Logout.tsx | 37 ++ apps/app/src/components/Layouts/Meta.tsx | 22 + .../app/src/components/Layouts/UserLayout.tsx | 316 ++++++++++++ apps/app/src/components/Layouts/UserMenu.tsx | 117 +++++ apps/app/src/components/Plans/Listing.tsx | 113 +++++ apps/app/src/components/SubscriptionStats.tsx | 100 ++++ .../src/components/common/LoadingCircular.tsx | 10 +- .../skeleton/common/CircularLoader.tsx | 20 + .../src/components/skeleton/common/Loader.tsx | 42 ++ apps/app/src/hooks/useAuth.ts | 79 +++ apps/app/src/hooks/useStorage.ts | 29 ++ apps/app/src/pages/_app.tsx | 9 - apps/app/src/pages/address/[id].tsx | 12 +- apps/app/src/pages/apis.tsx | 83 +++- apps/app/src/pages/campaign/[id].tsx | 141 ++++++ apps/app/src/pages/campaign/chart.tsx | 153 ++++++ apps/app/src/pages/campaign/index.tsx | 125 +++++ apps/app/src/pages/campaign/plans.tsx | 89 ++++ apps/app/src/pages/confirmemail.tsx | 254 ++++++++++ apps/app/src/pages/login.tsx | 455 ++++++++++++++++++ apps/app/src/pages/lostpassword.tsx | 205 ++++++++ apps/app/src/pages/nft-token/[id]/index.tsx | 11 + apps/app/src/pages/plans/index.tsx | 90 ++++ .../src/pages/publisher/adsubscriptions.tsx | 287 +++++++++++ .../src/pages/publisher/apisubscriptions.tsx | 348 ++++++++++++++ apps/app/src/pages/publisher/apiusage.tsx | 102 ++++ apps/app/src/pages/publisher/keys.tsx | 315 ++++++++++++ apps/app/src/pages/register.tsx | 404 ++++++++++++++++ apps/app/src/pages/resend.tsx | 175 +++++++ apps/app/src/pages/reset.tsx | 203 ++++++++ apps/app/src/pages/token/[id].tsx | 11 + apps/app/src/pages/updateemail.tsx | 208 ++++++++ apps/app/src/pages/user/apiusage.tsx | 104 ++++ apps/app/src/pages/user/invoices.tsx | 278 +++++++++++ apps/app/src/pages/user/keys.tsx | 316 ++++++++++++ apps/app/src/pages/user/overview.tsx | 170 +++++++ apps/app/src/pages/user/plan.tsx | 284 +++++++++++ apps/app/src/pages/user/settings.tsx | 110 +++++ apps/app/src/stores/withAuth.ts | 61 +++ apps/app/src/utils/libs.ts | 69 ++- apps/app/src/utils/types.ts | 48 ++ apps/app/src/utils/validationSchema.ts | 218 +++++++++ yarn.lock | 94 +++- 93 files changed, 11101 insertions(+), 145 deletions(-) create mode 100644 apps/app/src/components/ApiUsage/Chart.tsx create mode 100644 apps/app/src/components/ApiUsage/Stats.tsx create mode 100644 apps/app/src/components/Campaign/Advertiser/ConfirmModal.tsx create mode 100644 apps/app/src/components/Campaign/Advertiser/Listing.tsx create mode 100644 apps/app/src/components/Campaign/Advertiser/Stats.tsx create mode 100644 apps/app/src/components/Campaign/BannerAdForm.tsx create mode 100644 apps/app/src/components/Campaign/Plans.tsx create mode 100644 apps/app/src/components/Campaign/PreviewAd.tsx create mode 100644 apps/app/src/components/Campaign/Publisher/ConfirmModal.tsx create mode 100644 apps/app/src/components/Campaign/Publisher/Listing.tsx create mode 100644 apps/app/src/components/Campaign/Publisher/Stats.tsx create mode 100644 apps/app/src/components/Campaign/StartForm.tsx create mode 100644 apps/app/src/components/Campaign/TextAdForm.tsx create mode 100644 apps/app/src/components/CampaignPagination.tsx create mode 100644 apps/app/src/components/Chart.tsx create mode 100644 apps/app/src/components/Dashboard/Addkeys.tsx create mode 100644 apps/app/src/components/Dashboard/DeleteAccount.tsx create mode 100644 apps/app/src/components/Dashboard/DeleteKey.tsx create mode 100644 apps/app/src/components/Dashboard/Pagination.tsx create mode 100644 apps/app/src/components/Dashboard/SetPassword.tsx create mode 100644 apps/app/src/components/Dashboard/UpdateEmail.tsx create mode 100644 apps/app/src/components/Dashboard/UpdatePassword.tsx create mode 100644 apps/app/src/components/Icons/Avatar.tsx create mode 100644 apps/app/src/components/Icons/Campaign.tsx create mode 100644 apps/app/src/components/Icons/CircleTimer.tsx create mode 100644 apps/app/src/components/Icons/Close.tsx create mode 100644 apps/app/src/components/Icons/Create.tsx create mode 100644 apps/app/src/components/Icons/Delete.tsx create mode 100644 apps/app/src/components/Icons/Edit.tsx create mode 100644 apps/app/src/components/Icons/EmailCircle.tsx create mode 100644 apps/app/src/components/Icons/FaDownload.tsx create mode 100644 apps/app/src/components/Icons/FaFileInvoiceDollar.tsx create mode 100644 apps/app/src/components/Icons/FaUserAlt .tsx create mode 100644 apps/app/src/components/Icons/FileSearch.tsx create mode 100644 apps/app/src/components/Icons/Home.tsx create mode 100644 apps/app/src/components/Icons/Key.tsx create mode 100644 apps/app/src/components/Icons/LockCircle.tsx create mode 100644 apps/app/src/components/Icons/LoginCircle.tsx create mode 100644 apps/app/src/components/Icons/Logout.tsx create mode 100644 apps/app/src/components/Icons/Plan.tsx create mode 100644 apps/app/src/components/Icons/Refresh.tsx create mode 100644 apps/app/src/components/Icons/UnlockCircle.tsx create mode 100644 apps/app/src/components/Icons/Visibility.tsx create mode 100644 apps/app/src/components/Icons/VisibilityOff.tsx create mode 100644 apps/app/src/components/Layouts/Logout.tsx create mode 100644 apps/app/src/components/Layouts/Meta.tsx create mode 100644 apps/app/src/components/Layouts/UserLayout.tsx create mode 100644 apps/app/src/components/Layouts/UserMenu.tsx create mode 100644 apps/app/src/components/Plans/Listing.tsx create mode 100644 apps/app/src/components/SubscriptionStats.tsx create mode 100644 apps/app/src/components/skeleton/common/CircularLoader.tsx create mode 100644 apps/app/src/components/skeleton/common/Loader.tsx create mode 100644 apps/app/src/hooks/useAuth.ts create mode 100644 apps/app/src/hooks/useStorage.ts create mode 100644 apps/app/src/pages/campaign/[id].tsx create mode 100644 apps/app/src/pages/campaign/chart.tsx create mode 100644 apps/app/src/pages/campaign/index.tsx create mode 100644 apps/app/src/pages/campaign/plans.tsx create mode 100644 apps/app/src/pages/confirmemail.tsx create mode 100644 apps/app/src/pages/login.tsx create mode 100644 apps/app/src/pages/lostpassword.tsx create mode 100644 apps/app/src/pages/plans/index.tsx create mode 100644 apps/app/src/pages/publisher/adsubscriptions.tsx create mode 100644 apps/app/src/pages/publisher/apisubscriptions.tsx create mode 100644 apps/app/src/pages/publisher/apiusage.tsx create mode 100644 apps/app/src/pages/publisher/keys.tsx create mode 100644 apps/app/src/pages/register.tsx create mode 100644 apps/app/src/pages/resend.tsx create mode 100644 apps/app/src/pages/reset.tsx create mode 100644 apps/app/src/pages/updateemail.tsx create mode 100644 apps/app/src/pages/user/apiusage.tsx create mode 100644 apps/app/src/pages/user/invoices.tsx create mode 100644 apps/app/src/pages/user/keys.tsx create mode 100644 apps/app/src/pages/user/overview.tsx create mode 100644 apps/app/src/pages/user/plan.tsx create mode 100644 apps/app/src/pages/user/settings.tsx create mode 100644 apps/app/src/stores/withAuth.ts create mode 100644 apps/app/src/utils/validationSchema.ts diff --git a/apps/app/package.json b/apps/app/package.json index beb39742..361b1169 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -55,6 +55,11 @@ "clipboard": "^2.0.11", "dayjs": "1.11.0", "ethers": "^5.7.2", + "formik": "^2.4.6", + "highcharts": "^11.4.8", + "highcharts-exporting": "^0.1.7", + "highcharts-react-official": "^3.2.1", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "near-api-js": "5.0.0", "near-social-vm": "github:NearSocial/VM#2.5.5", @@ -67,7 +72,9 @@ "qs": "^6.12.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-google-recaptcha": "^3.1.0", "react-hook-form": "7.46.1", + "react-loading-skeleton": "^3.4.0", "react-perfect-scrollbar": "^1.5.8", "react-singleton-hook": "4.0.1", "react-syntax-highlighter": "^15.5.0", @@ -78,11 +85,13 @@ }, "devDependencies": { "@types/big.js": "~6.2.2", + "@types/js-cookie": "^3.0.6", "@types/lodash": "~4.17.7", "@types/node": "~20.8", "@types/qs": "~6.9.15", "@types/react": "~18.2", "@types/react-dom": "~18.2", + "@types/react-google-recaptcha": "^2.1.9", "@types/react-syntax-highlighter": "^15.5.13", "autoprefixer": "~10.4", "eslint-config-custom-nextjs": "*", diff --git a/apps/app/src/components/ActiveLink.tsx b/apps/app/src/components/ActiveLink.tsx index babd88a0..4ec99143 100644 --- a/apps/app/src/components/ActiveLink.tsx +++ b/apps/app/src/components/ActiveLink.tsx @@ -8,6 +8,7 @@ interface ActiveLinkProps extends LinkProps { activeClassName?: string; inActiveClassName?: string; href: string | UrlObject; + exact?: boolean | null; } const ActiveLink = ({ @@ -15,6 +16,7 @@ const ActiveLink = ({ activeClassName, inActiveClassName, href, + exact, ...props }: ActiveLinkProps) => { const { asPath } = useRouter(); @@ -23,10 +25,13 @@ const ActiveLink = ({ const childClassName = child?.props?.className || ' '; const hrefString = typeof href === 'string' ? href : href.pathname || ''; - - const className = ( - href === '/' ? asPath === href : asPath.startsWith(hrefString) - ) + let isActive = false; + if (exact) { + isActive = asPath === href; + } else { + isActive = href === '/' ? asPath === href : asPath.startsWith(hrefString); + } + const className = isActive ? `${childClassName} ${activeClassName}` : `${childClassName} ${inActiveClassName}`; diff --git a/apps/app/src/components/ApiUsage/Chart.tsx b/apps/app/src/components/ApiUsage/Chart.tsx new file mode 100644 index 00000000..cae04cae --- /dev/null +++ b/apps/app/src/components/ApiUsage/Chart.tsx @@ -0,0 +1,245 @@ +import React, { useEffect, useState } from 'react'; +import { useTheme } from 'next-themes'; +import Highcharts from 'highcharts/highstock'; +import HighchartsReact from 'highcharts-react-official'; +import HighchartsExporting from 'highcharts/modules/exporting'; +import dayjs from '../../utils/dayjs'; +import withAuth from '@/stores/withAuth'; +import useAuth from '@/hooks/useAuth'; +import Plan from '../Icons/Plan'; +import CircularLoader from '../skeleton/common/CircularLoader'; +import { TooltipFormatterContextObject } from 'highcharts'; + +const ApiUsageChart = ({ keyId }: { keyId?: string | string[] }) => { + const [options, setOptions] = useState(null); + const [loading, setLoading] = useState(true); + const { theme } = useTheme(); + const { data: chartData, loading: loadingChart } = useAuth( + keyId ? `/key-usage/${keyId}` : '', + ); + useEffect(() => { + const isDarkTheme = theme === 'dark'; + if (!chartData) return; + const chartOptions: any = { + chart: { + backgroundColor: isDarkTheme ? '#0d0d0d' : '#fff', + height: 500, + zoomType: 'x', + }, + title: { + text: 'Overview', + style: { + color: isDarkTheme ? '#FFFFFF' : '#000000', + }, + }, + xAxis: { + type: 'datetime', + labels: { + style: { + color: isDarkTheme ? '#CCCCCC' : '#666666', + }, + }, + lineColor: isDarkTheme ? '#666666' : '#cccccc', + tickColor: isDarkTheme ? '#666666' : '#cccccc', + gridLineColor: isDarkTheme ? '#444444' : '#E6E6E6', + }, + yAxis: { + title: { + text: null, + style: { + color: isDarkTheme ? '#FFFFFF' : '#000000', + }, + }, + labels: { + style: { + color: isDarkTheme ? '#CCCCCC' : '#666666', + }, + }, + gridLineColor: isDarkTheme ? '#444444' : '#e6e6e6', + }, + legend: { + enabled: false, + itemStyle: { + color: isDarkTheme ? '#CCCCCC' : '#666666', + }, + }, + plotOptions: { + area: { + fillColor: { + linearGradient: { + x1: 0, + y1: 0, + x2: 0, + y2: 1, + }, + stops: [ + [ + 0, + isDarkTheme + ? 'rgba(30, 130, 133, 0.8)' + : 'rgba(3, 63, 64, 0.8)', + ], + [1, isDarkTheme ? 'rgba(3, 63, 64, 0.20)' : 'rgba(3, 63, 64, 0)'], + ], + }, + marker: { + enabled: false, + }, + lineWidth: 1, + states: { + hover: { + lineWidth: 1, + }, + }, + threshold: null, + turboThreshold: 3650, + }, + }, + tooltip: { + shared: true, + valueSuffix: '', + formatter: function (this: TooltipFormatterContextObject) { + let s = `${dayjs(this.x).format('MM/DD/YYYY HH:mm')}`; + this?.points?.forEach(function (point) { + s += `
\u25CF ${point.series.name}: ${point.y}`; + }); + return s; + }, + }, + series: [ + { + name: 'API Key Usage', + type: 'area', + data: chartData?.length > 0 ? chartData : [], + color: isDarkTheme + ? 'rgba(30, 130, 133, 0.8)' + : 'rgba(3, 63, 64, 0.8)', + fillColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [ + 0, + isDarkTheme + ? 'rgba(30, 130, 133, 0.8)' + : 'rgba(3, 63, 64, 0.8)', + ], + [1, isDarkTheme ? 'rgba(3, 63, 64, 0.20)' : 'rgba(3, 63, 64, 0)'], + ], + }, + }, + ], + credits: { + enabled: false, + }, + rangeSelector: { + allButtonsEnabled: true, + selected: 0, + buttonTheme: { + width: 60, + }, + }, + exporting: { + menuItemDefinitions: { + embed: { + onclick: function () { + open; + }, + text: 'Embed chart', + }, + }, + buttons: { + contextButton: { + menuItems: [ + 'viewFullscreen', + 'printChart', + 'separator', + 'downloadPNG', + 'downloadJPEG', + 'downloadPDF', + 'downloadSVG', + 'separator', + 'embed', + ], + theme: { + fill: isDarkTheme ? '#444444' : '#FFFFFF', // Button background + style: { + color: isDarkTheme ? '#FFFFFF' : '#000000', // Text color + }, + states: { + hover: { + fill: isDarkTheme ? '#555555' : '#E6E6E6', // Hover background + style: { + color: isDarkTheme ? '#FFFFFF' : '#000000', // Hover text color + }, + }, + select: { + fill: isDarkTheme ? '#333333' : '#E6E6E6', // Select background + style: { + color: isDarkTheme ? '#FFFFFF' : '#000000', // Select text color + }, + }, + }, + }, + }, + }, + }, + }; + const wrapper = document.getElementById('chart-wrapper'); + if (wrapper) { + chartOptions.chart.height = wrapper.offsetWidth; + chartOptions.chart.height = wrapper.offsetHeight; + } + setOptions(chartOptions); + setLoading(false); + }, [chartData, theme]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (typeof Highcharts === 'object') { + HighchartsExporting(Highcharts); + } + + return ( + <> + {loadingChart ? ( +
+ +
+ ) : chartData?.length > 0 ? ( +
+
+ +
+
+ ) : ( +
+
+
+ + + +
+

+ API Key Usage +

+

+ No API key usage over time +

+
+
+ )} + + ); +}; +export default withAuth(ApiUsageChart); diff --git a/apps/app/src/components/ApiUsage/Stats.tsx b/apps/app/src/components/ApiUsage/Stats.tsx new file mode 100644 index 00000000..80d91719 --- /dev/null +++ b/apps/app/src/components/ApiUsage/Stats.tsx @@ -0,0 +1,179 @@ +import React, { useMemo } from 'react'; +import useAuth from '@/hooks/useAuth'; +import { localFormat } from '@/utils/libs'; +import Skeleton from '../skeleton/common/Skeleton'; + +const ApiUsageStats = ({ keyId }: { keyId?: string | string[] }) => { + const { data, loading } = useAuth('advertiser/api-usage/stats'); + const { data: keyData, loading: keyDataLoading } = useAuth( + keyId ? `api-usage-per-key/${keyId}/stats` : '', + ); + const rateLimit = useMemo(() => { + const limit = data?.monthLimit ? data?.monthLimit : 0; + const dayLimit = data?.dayLimit ? data?.dayLimit : 0; + const consumed = data?.consumed ? data?.consumed : 0; + const minuteLimit = data?.minuteLimit ? data?.minuteLimit : 0; + const consumedDaily = data?.consumedDaily ? data?.consumedDaily : 0; + + const adjustedConsumed = Math.min(consumed, limit); + const adjustedConsumedDaily = Math.min(consumedDaily, dayLimit); + + const percentage = limit === 0 ? 0 : (adjustedConsumed / limit) * 100; + const dailyPercentage = + dayLimit === 0 ? 0 : (adjustedConsumedDaily / dayLimit) * 100; + + return { + limit, + consumed: adjustedConsumed, + percentage: Math.min(percentage, 100), + dailyPercentage: Math.min(dailyPercentage, 100), + dayLimit, + consumedDaily: adjustedConsumedDaily, + minuteLimit, + }; + }, [data]); + + const isDailyLimitExceeded = rateLimit?.consumedDaily >= rateLimit?.dayLimit; + const isMonthlyLimitExceeded = rateLimit?.consumed >= rateLimit?.limit; + + return ( + <> + { +
+
+
+
+

+ {'Requests Consumed (Monthly)'} +

+ {keyId ? ( + !keyDataLoading ? ( + <> +

+ {localFormat(keyData?.consumed)} +

+
+

+ Current rate limit: {data?.minuteLimit} req/min +

+
+ + ) : ( + <> + + + + ) + ) : !loading ? ( + <> +

+ {localFormat(data?.consumed)} +

+
+

+ Current rate limit: {data?.minuteLimit} req/min +

+
+ + ) : ( + <> + + + + )} +
+
+
+
+

+ {'Requests Remaining (24H)'} +

+ {!loading ? ( + <> +

+ {localFormat(data?.remainingDaily)} +

+
+
+
+
+
+

+ {localFormat(String(rateLimit?.consumedDaily))} / + {localFormat(rateLimit?.dayLimit)} +

+

+ Daily (24H) Quota +

+
+
+ {isDailyLimitExceeded && ( +

+ You have exceeded your daily API request limit! +

+ )} + + ) : ( + <> + + + + )} +
+
+
+
+

+ {'Requests Remaining (Monthly)'} +

+ {!loading ? ( + <> +

+ {localFormat(data?.remainingMonthly)} +

+
+
+
+
+
+

+ {localFormat(String(rateLimit?.consumed))} / + {localFormat(rateLimit?.limit)} +

+

+ Monthly Quota +

+
+
+ {isMonthlyLimitExceeded && ( +

+ You have exceeded your monthly API request limit! +

+ )} + + ) : ( + <> + + + + )} +
+
+
+
+ } + + ); +}; + +export default ApiUsageStats; diff --git a/apps/app/src/components/Campaign/Advertiser/ConfirmModal.tsx b/apps/app/src/components/Campaign/Advertiser/ConfirmModal.tsx new file mode 100644 index 00000000..6e7b29df --- /dev/null +++ b/apps/app/src/components/Campaign/Advertiser/ConfirmModal.tsx @@ -0,0 +1,90 @@ +import { DialogOverlay, DialogContent } from '@reach/dialog'; +import { toast } from 'react-toastify'; +import { useState } from 'react'; +import Close from '@/components/Icons/Close'; +import { currentCampaign } from '@/utils/types'; + +type Props = { + setConfirmOpen: React.Dispatch>; + currentCampaign: currentCampaign | null; + mutate: () => void; + handleCampaignCancellation: () => Promise; + buttonLoading: boolean; + setButtonLoading: React.Dispatch>; +}; + +const ConfirmModal = ({ + setConfirmOpen, + handleCampaignCancellation, +}: Props) => { + const [buttonLoading, setButtonLoading] = useState(false); + + const onSubmit = async () => { + try { + setButtonLoading(true); + await handleCampaignCancellation(); + if (!toast.isActive('campaign-cancelled')) { + toast.success('Campaign cancelled successfully!', { + toastId: 'campaign-cancelled', + }); + } + setConfirmOpen(false); + } catch (error) { + if (!toast.isActive('campaign-cancelled-error')) { + toast.error('Failed to cancel campaign', { + toastId: 'campaign-cancelled-error', + }); + } + } finally { + setButtonLoading(false); + } + }; + + return ( + <> + + +
+

+ Confirm Campaign Canceling +

+ +
+
+
+

+ Are you sure you want to cancel this campaign? Canceling this + campaign will also cancel your subscription!{' '} +

+
+
+

setConfirmOpen(false)} + className="text-[13px] delay-300 duration-300 mx-1 dark:text-neargray-10 hover:bg-gray-200 dark:hover:bg-black-200 px-4 py-2 rounded cursor-pointer" + > + Cancel +

+ +
+
+
+
+ + ); +}; +export default ConfirmModal; diff --git a/apps/app/src/components/Campaign/Advertiser/Listing.tsx b/apps/app/src/components/Campaign/Advertiser/Listing.tsx new file mode 100644 index 00000000..c56f5ba1 --- /dev/null +++ b/apps/app/src/components/Campaign/Advertiser/Listing.tsx @@ -0,0 +1,279 @@ +import React, { useEffect, useState } from 'react'; +import Link from 'next/link'; +import ConfirmModal from './ConfirmModal'; + +import { useRouter } from 'next/router'; +import useAuth, { request } from '@/hooks/useAuth'; +import Edit from '@/components/Icons/Edit'; +import Plan from '@/components/Icons/Plan'; +import { localFormat } from '@/utils/libs'; +import { currentCampaign } from '@/utils/types'; +import CampaignPagination from '@/components/CampaignPagination'; +import Skeleton from '@/components/skeleton/common/Skeleton'; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +type Props = { + statsMutate: () => void; +}; + +const Listing = ({ statsMutate }: Props) => { + const [confirmOpen, setConfirmOpen] = useState(false); + const [currentCampaign, setCurrentCampaign] = + useState(null); + const [buttonLoading, setButtonLoading] = useState(false); + const [url, setUrl] = useState(`advertiser/campaigns?page=1`); + const router = useRouter(); + + const { data, loading, mutate } = useAuth(url); + + const { status } = router.query; + + useEffect(() => { + if (status) { + mutate(); + } + }, [status, mutate]); + + const formatStartDate = (startDate: string) => { + if (!startDate) return 'XX/XX/XXXX'; + + const date = new Date(startDate); + const timeZone = 'UTC'; // Set your desired time zone here + const formattedDate = date.toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + timeZone, + }); + + return formattedDate; + }; + + const onCancel = (item: currentCampaign) => { + setConfirmOpen(true); + setCurrentCampaign(item); + }; + + const handleCampaignCancellation = async () => { + await request.post(`advertiser/campaign/${currentCampaign?.id}/cancel`); + await sleep(1000); + mutate(); + statsMutate(); + }; + + return ( + <> +
+
+
+
+
+

+ My Campaigns +

+
+

+ Below are your campaigns. Click on "Edit" to view + and modify the campaign. +

+
+
+ +
+
+ + + + + + + + + + + + + {loading && + [...Array(4)].map((_, i) => ( + + + + + + + + + ))} + {!loading && data?.data?.length === 0 && ( + + + + )} + {data?.data?.map((item: currentCampaign) => ( + + + + + + + + + ))} + +
+ placement + + status + + impressions + + clicks + + start date + + actions +
+ + + + + + + + + + + +
+
+
+
+ + + +
+

+ Campaigns Empty +

+

+ No Campaign Found +

+
+
+
+ + {item?.title.charAt(0).toUpperCase() + + item?.title.slice(1)} + + + + {item?.is_active == 1 ? 'active' : 'non-active'} + + + + {localFormat((item?.impression_count).toString())} + + + + {localFormat((item?.click_count).toString())} + + + + {formatStartDate(item?.start_date)} + + +
+ + {' '} +   +

+ Edit +

+ +
+
+ +

+ Stats +

+ +
+ {item?.subscription?.status === 'canceled' ? ( + + Cancelled + + ) : ( +
onCancel(item)} + className="flex items-center border border-green-500 dark:border-green-250 rounded-md px-2 py-1 hover:bg-primary-100 dark:hover:bg-black-200" + > +

+ Cancel Campaign +

+
+ )} +
+
+
+
+
+ {confirmOpen && ( + + )} + + ); +}; +export default Listing; diff --git a/apps/app/src/components/Campaign/Advertiser/Stats.tsx b/apps/app/src/components/Campaign/Advertiser/Stats.tsx new file mode 100644 index 00000000..94f37297 --- /dev/null +++ b/apps/app/src/components/Campaign/Advertiser/Stats.tsx @@ -0,0 +1,220 @@ +import React, { useEffect } from 'react'; + +import useAuth from '@/hooks/useAuth'; +import { localFormat } from '@/utils/libs'; +import Skeleton from '@/components/skeleton/common/Skeleton'; + +type Props = { + campaignId?: string | string[]; + statsMutate?: () => void; + isTextHide?: boolean; +}; + +const AdImpressions = ({ campaignId, statsMutate, isTextHide }: Props) => { + const { data, loading } = useAuth('advertiser/stats'); + const { data: campaignStats, loading: campaignDataLoading } = useAuth( + campaignId ? `campaign/${campaignId}/stats` : '', + ); + + const { data: campaignOverAllStats, loading: campaignOverAllDataLoading } = + useAuth(campaignId ? `campaign/${campaignId}/overall-stats` : ''); + + let totalImpression = +data?.totalImpression ? +data?.totalImpression : '0'; + let totalClicks = +data?.totalClicks ? +data?.totalClicks : '0'; + let totalSpent = +data?.totalSpent / 100 ? +data?.totalSpent / 100 : '$0'; + + useEffect(() => { + if (statsMutate) { + statsMutate(); + } + }, [statsMutate, campaignId]); + + return ( + <> + {!isTextHide ? ( +
+
+

+ Total Impressions + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignDataLoading ? ( + localFormat(campaignStats?.totalImpression) + ) : ( + + ) + ) : ( + localFormat(totalImpression.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Total Clicks{' '} + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignDataLoading ? ( + localFormat(campaignStats?.totalClicks) + ) : ( + + ) + ) : ( + localFormat(totalClicks.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Active Subscription{' '} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignDataLoading ? ( + campaignStats?.activeSubscription?.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }) + ) : ( + + ) + ) : ( + totalSpent?.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }) + )} +

+ ) : ( + + )} +
+
+ ) : ( +
+
+

+ Total Impressions{' '} + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignOverAllDataLoading ? ( + localFormat(campaignOverAllStats?.totalImpression) + ) : ( + + ) + ) : ( + localFormat(totalImpression.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Total Clicks{' '} + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignOverAllDataLoading ? ( + localFormat(campaignOverAllStats?.totalClicks) + ) : ( + + ) + ) : ( + localFormat(totalClicks.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Active Subscription{' '} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignOverAllDataLoading ? ( + campaignOverAllStats?.activeSubscription?.toLocaleString( + 'en-US', + { + style: 'currency', + currency: 'USD', + }, + ) + ) : ( + + ) + ) : ( + totalSpent?.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }) + )} +

+ ) : ( + + )} +
+
+ )} + + ); +}; + +export default AdImpressions; diff --git a/apps/app/src/components/Campaign/BannerAdForm.tsx b/apps/app/src/components/Campaign/BannerAdForm.tsx new file mode 100644 index 00000000..9a9e3e2e --- /dev/null +++ b/apps/app/src/components/Campaign/BannerAdForm.tsx @@ -0,0 +1,351 @@ +/* eslint-disable @next/next/no-img-element */ +import React, { useEffect, useState } from 'react'; +import { FormikValues, useFormik } from 'formik'; +import { toast } from 'react-toastify'; +import StartForm from './StartForm'; +import Avatar from '../Icons/Avatar'; +import { request } from '@/hooks/useAuth'; +import LoginCircle from '../Icons/LoginCircle'; +import EmailCircle from '../Icons/EmailCircle'; +import { bannerCampaignValidation } from '@/utils/validationSchema'; +import { CampaignProps } from '@/utils/types'; +import Skeleton from '../skeleton/common/Skeleton'; + +type BannerPreview = { + desktopRight?: string; + mobile?: string; +}; + +const BannerAdForm = ({ + campaignId, + campaignData, + mutate, + loading, + campaignMutate, +}: CampaignProps) => { + const validationSchema = bannerCampaignValidation(campaignData); + const [bannerPreview, setBannerPreview] = useState({}); + const [banner, setBanner] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubscriptionCancelled, setIsSubscriptionCancelled] = useState(false); + + useEffect(() => { + if (campaignId && campaignData) { + const mobUrl = campaignData?.data?.mobile_image_url; + const desktopRightUrl = campaignData?.data?.desktop_image_right_url; + + setBannerPreview({ + desktopRight: desktopRightUrl, + mobile: mobUrl, + }); + + formik.setFieldValue('desktop_image_right', desktopRightUrl); + formik.setFieldValue('mobile_image', mobUrl); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [campaignId, campaignData]); + + useEffect(() => { + if (campaignData?.data?.subscription?.status === 'canceled') { + setIsSubscriptionCancelled(true); + } else { + setIsSubscriptionCancelled(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [campaignData?.data?.subscription?.status]); + + const onSubmit = async (values: FormikValues) => { + const formData = new FormData(); + formData.append('is_active', '0'); + formData.append('url', values?.url); + // Only append images if it's a File (indicating it has been changed) + if (values?.desktop_image_right instanceof File) { + formData.append('desktop_image_right', values?.desktop_image_right); + } + if (values?.mobile_image instanceof File) { + formData.append('mobile_image', values?.mobile_image); + } + setIsSubmitting(true); + try { + await request.post(`/campaigns/${campaignId}`, formData); + if (!toast.isActive('campaign-update')) { + toast.success('Campaign Information updated', { + toastId: 'campaign-update', + }); + } + setIsSubmitting(false); + mutate(); + campaignMutate(); + } catch (error: any) { + if (error?.response?.data?.message) { + if (!toast.isActive('campaign-update-submit-error')) { + toast.error(error?.response?.data?.message, { + toastId: 'campaign-update-submit-error', + }); + } + } else { + if (!toast.isActive('campaign-update')) { + toast.error('Something went wrong', { + toastId: 'campaign-update-error', + }); + } + } + setIsSubmitting(false); + } + }; + + const handleDesktopImageRightChange = ( + e: React.ChangeEvent, + ) => { + formik.setFieldTouched('desktop_image_right', true, false); + if (e.target.files && e.target.files[0]) { + const url = URL.createObjectURL(e.target.files[0]); + setBanner({ ...banner, desktopRight: e.target.files[0] }); + setBannerPreview({ ...bannerPreview, desktopRight: url }); + formik.setFieldValue('desktop_image_right', e.target.files[0]); + } + }; + + const handleMobileImageChange = (e: React.ChangeEvent) => { + formik.setFieldTouched('mobile_image', true, false); + if (e.target.files && e.target.files[0]) { + const url = URL.createObjectURL(e.target.files[0]); + setBanner({ ...banner, mobile: e.target.files[0] }); + setBannerPreview({ ...bannerPreview, mobile: url }); + formik.setFieldValue('mobile_image', e.target.files[0]); + } + }; + + const formik = useFormik({ + initialValues: { + url: campaignData?.data?.url || '', + desktop_image_right: campaignData?.data?.desktop_image_right_url || null, + mobile_image: campaignData?.data?.mobile_image_url || null, + }, + validationSchema, + onSubmit, + validateOnChange: true, + validateOnBlur: true, + enableReinitialize: true, + }); + + return ( + <> +
+
+
+

Campaign Information

+
+

+ Edit the below campaign informations +

+
+
+
+

+ Placement +

+ {loading ? ( +
+ +
+ ) : ( +

+ {campaignData?.data?.title} +

+ )} +
+
+ {campaignData?.data?.start_date && ( +
+
+

+ Start Date +

+ {loading ? ( +
+ +
+ ) : ( +

+ {`${campaignData?.data?.start_date.slice(0, 19)} (UTC)`} +

+ )} +
+
+ )} + +
+

+ URL +

+ {loading ? ( +
+ +
+ ) : ( +
+ + {formik?.touched?.url && formik?.errors?.url && ( + + {formik?.errors?.url} + + )} +
+ )} +
+
+
+
+ {' '} + + Desktop version - Right (320px - 100px) + +
+

+ (* Accept only .png, .jpg, .jpeg, .gif, .webp, .svg + extensions) +

+
+ {loading ? ( +
+ +
+ ) : ( +
+
+ +
+ {formik?.touched?.desktop_image_right && + formik?.errors?.desktop_image_right && ( + + {formik?.errors?.desktop_image_right} + + )} +
+ )} +
+
+
+
+ {' '} + Mobile version (730px - 90px) +
+

+ (* Accept only .png, .jpg, .jpeg, .gif, .webp, .svg + extensions) +

+
+ {loading ? ( +
+ +
+ ) : ( +
+
+ +
+ {formik?.touched?.mobile_image && + formik?.errors?.mobile_image && ( + + {formik?.errors?.mobile_image} + + )} +
+ )} +
+
+ +
+
+
+
+ +
+ +
+ + ); +}; +export default BannerAdForm; diff --git a/apps/app/src/components/Campaign/Plans.tsx b/apps/app/src/components/Campaign/Plans.tsx new file mode 100644 index 00000000..8aacf2b4 --- /dev/null +++ b/apps/app/src/components/Campaign/Plans.tsx @@ -0,0 +1,216 @@ +import React, { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useRouter } from 'next/router'; +import { catchErrors, dollarFormat } from '@/utils/libs'; +import { currentCampaign } from '@/utils/types'; +import useAuth, { request } from '@/hooks/useAuth'; +import SwitchButton from '../SwitchButton'; +import LoadingCircular from '../common/LoadingCircular'; +import Skeleton from '../skeleton/common/Skeleton'; + +interface CampaignDetail { + currentPlan?: currentCampaign; +} + +const CampaignPlans = () => { + const [_currentPlanId, setCurrentPlanId] = useState(''); + const [isSubmitting, setIsSubmitting] = useState<{ [key: string]: boolean }>( + {}, + ); + const [interval, setInterval] = useState(false); + const [purchasedPlanId, setPurchasedPlanId] = useState(); + const router = useRouter(); + + const { data, mutate } = useAuth('/campaign/plans'); + const { data: userData, mutate: campaignMutate } = useAuth( + '/campaign/subscription-info', + { + revalidateOnMount: true, + }, + ); + const currentPlans = userData?.campaignDetails?.map( + (detail: CampaignDetail) => detail?.currentPlan?.id, + ); + + const { status } = router.query; + + const onGetStarted = async (plan: currentCampaign): Promise => { + setCurrentPlanId(plan?.id); + localStorage.setItem('purchased-plan-id', plan?.id); + setIsSubmitting((prev) => ({ ...prev, [plan?.id]: true })); + + try { + const res = await request.post(`advertiser/subscribe`, { + interval: !interval ? 'month' : 'year', + plan_id: plan?.id, + }); + setIsSubmitting((prev) => ({ ...prev, [plan?.id]: false })); + if (res?.data) { + await router.push(res?.data && res?.data['url ']); + } + mutate(); + campaignMutate(); + } catch (error) { + //const statusCode = get(error, "response.status") || null; + setCurrentPlanId(''); + const message = catchErrors(error); + if (message === 'You have already subscribed this plan') { + if (!toast.isActive('plan-subscrition-exists')) { + toast.error('You have already subscribed this plan', { + toastId: 'plan-subscrition-exists', + }); + } + } else { + if (!toast.isActive('plan-subscrition-error')) { + toast.error('Something went wrong, please try again later', { + toastId: 'plan-subscrition-error', + }); + } + } + setIsSubmitting((prev) => ({ ...prev, [plan?.id]: false })); + } + }; + + useEffect(() => { + const purchasedPlanId = localStorage.getItem('purchased-plan-id'); + setPurchasedPlanId(purchasedPlanId); + + if (purchasedPlanId) { + const timer = setTimeout(() => { + localStorage.removeItem('purchased-plan-id'); + }, 10000); + + return () => clearTimeout(timer); + } + return; + }, []); + + return ( +
+

+ Choose a plan that's right for you. +

+
+

+ Monthly{' '} +

+ + setInterval(!interval)} + /> + +

+ Annually{' '} +

+
+
+ {data?.data?.length + ? data?.data?.map((plan: currentCampaign, index: number) => ( +
+
+
+

{plan?.title}

+

+ {!interval ? ( +

+ $ + {dollarFormat((plan?.price_monthly / 100).toString())} + /mo +

+ ) : ( +

+ $ + {dollarFormat( + (plan?.price_annually / 100).toString(), + )} + /yr +

+ )} +

+
+
+
+

30 slots available

+

Rotate between each slot

+ +
+
+ )) + : [...Array(2)].map((_, i) => ( +
+
+

+ +

+

+ +

+
+
+

+ +

+

+ +

+
+ + + +
+
+
+ ))} +
+
+ ); +}; + +export default CampaignPlans; diff --git a/apps/app/src/components/Campaign/PreviewAd.tsx b/apps/app/src/components/Campaign/PreviewAd.tsx new file mode 100644 index 00000000..06c1684e --- /dev/null +++ b/apps/app/src/components/Campaign/PreviewAd.tsx @@ -0,0 +1,70 @@ +/* eslint-disable @next/next/no-img-element */ +import React from 'react'; +import { DialogOverlay, DialogContent } from '@reach/dialog'; +import Close from '../Icons/Close'; +import { CampaignData } from '@/utils/types'; + +interface Props { + onToggleAdd: () => void; + isOpen: boolean; + campaignData: CampaignData['data']; +} + +const PreviewAd = ({ onToggleAdd, isOpen, campaignData }: Props) => { + return ( + + +
+

+ Text Ad Preview +

+ +
+
+
+
+
+
+

+ Sponsored: {' '} + Icon{' '} + + {campaignData?.site_name} + + :{campaignData?.text}{' '} + + {campaignData?.link_name} + +

+
+
+
+
+
+
+
+ ); +}; + +export default PreviewAd; diff --git a/apps/app/src/components/Campaign/Publisher/ConfirmModal.tsx b/apps/app/src/components/Campaign/Publisher/ConfirmModal.tsx new file mode 100644 index 00000000..12fecb8e --- /dev/null +++ b/apps/app/src/components/Campaign/Publisher/ConfirmModal.tsx @@ -0,0 +1,74 @@ +import { DialogOverlay, DialogContent } from '@reach/dialog'; +import { toast } from 'react-toastify'; +import Close from '@/components/Icons/Close'; +import { request } from '@/hooks/useAuth'; +import { currentCampaign } from '@/utils/types'; + +type Props = { + setConfirmOpen: React.Dispatch>; + currentCampaign: currentCampaign | null; + mutate: () => void; +}; + +const ConfirmModal = ({ setConfirmOpen, currentCampaign, mutate }: Props) => { + const onSubmit = async () => { + try { + await request.post(`publisher/campaign/${currentCampaign?.id}/approve`); + if (!toast.isActive('campaign-approved')) { + toast.success('The campaign has been approved successfully', { + toastId: 'campaign-approved', + }); + } + setConfirmOpen(false); + mutate(); + } catch (error: any) { + if (!toast.isActive('campaign-approve-error')) { + toast.error(error?.response?.data, { + toastId: 'campaign-approve-error', + }); + } + setConfirmOpen(false); + } + }; + return ( + <> + + +
+

+ Confirm Campaign Approval +

+ +
+
+
+

+ Are you sure you want to approve this campaign?{' '} +

+
+
+

setConfirmOpen(false)} + className="text-[13px] delay-300 duration-300 mx-1 hover:bg-gray-200 dark:hover:bg-black-200 px-4 py-2 rounded" + > + Cancel +

+ +
+
+
+
+ + ); +}; +export default ConfirmModal; diff --git a/apps/app/src/components/Campaign/Publisher/Listing.tsx b/apps/app/src/components/Campaign/Publisher/Listing.tsx new file mode 100644 index 00000000..f1ed6c0f --- /dev/null +++ b/apps/app/src/components/Campaign/Publisher/Listing.tsx @@ -0,0 +1,284 @@ +import { useState } from 'react'; +import Link from 'next/link'; +import ConfirmModal from './ConfirmModal'; +import useAuth from '@/hooks/useAuth'; +import Edit from '@/components/Icons/Edit'; +import Plan from '@/components/Icons/Plan'; +import CampaignPagination from '@/components/CampaignPagination'; +import { localFormat } from '@/utils/libs'; +import { currentCampaign } from '@/utils/types'; +import Skeleton from '@/components/skeleton/common/Skeleton'; + +const CampaignListing = () => { + const [confirmOpen, setConfirmOpen] = useState(false); + const [currentCampaign, setCurrentCampaign] = + useState(null); + const [url, setUrl] = useState(`publisher/campaigns?page=1`); + const { data, loading, mutate } = useAuth(url); + + const formatStartDate = (startDate: string) => { + if (!startDate) return 'XX/XX/XXXX'; + + const date = new Date(startDate); + const timeZone = 'UTC'; // Set your desired time zone here + const formattedDate = date.toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + timeZone, + }); + + return formattedDate; + }; + + const onApprove = (item: currentCampaign) => { + setConfirmOpen(true); + setCurrentCampaign(item); + }; + + return ( + <> +
+
+
+

+ Campaign +

+

+

+ {' '} + Below are the campaigns. Click on "Edit" to view and + modify the campaign. +
+

+
+ +
+
+ + + + + + + + + + + + + + + {loading && + [...Array(4)].map((_, i) => ( + + + + + + + + + + + ))} + {!loading && data?.data?.length === 0 && ( + + + + )} + {data?.data?.map((item: currentCampaign) => ( + + + + + + + + + + + ))} + +
+ placement + + plan + + advertiser + + impressions + + clicks + + start date + + status + + action +
+ + + + + + + + + + + + + + + +
+
+
+
+ + + +
+

+ Campaigns Empty +

+

+ No Campaign Found +

+
+
+
+ + {' '} + {item?.title.charAt(0).toUpperCase() + + item?.title.slice(1)} + + + + {item?.subscription?.campaign_plan?.title && + item?.subscription?.campaign_plan?.title + .charAt(0) + .toUpperCase() + + item?.subscription?.campaign_plan?.title.slice(1)} + + + {item?.user?.username} + + + {localFormat((item?.impression_count).toString())} + + + + {localFormat((item?.click_count).toString())} + + + + {formatStartDate(item?.start_date)} + + + + {item?.is_active ? 'ACTIVE' : 'NON-ACTIVE'} + + +
+ + {' '} +   +

+ Edit +

+ +
+
+ +

+ Stats +

+ +
+ {item?.subscription?.status === 'canceled' ? ( + + Cancelled + + ) : ( + item?.title != 'Placeholder Ad' && + item?.title != 'Placeholder Text Ad' && + (item?.is_approved == 1 ? ( + + Approved + + ) : ( +
onApprove(item)} + className="flex items-center border border-green-500 dark:border-green-250 rounded-md px-2 py-1 hover:bg-primary-100 dark:hover:bg-black-200" + > +

+ Approve +

+
+ )) + )} +
+
+
+ + {confirmOpen && ( + + )} + + ); +}; +export default CampaignListing; diff --git a/apps/app/src/components/Campaign/Publisher/Stats.tsx b/apps/app/src/components/Campaign/Publisher/Stats.tsx new file mode 100644 index 00000000..b1645e76 --- /dev/null +++ b/apps/app/src/components/Campaign/Publisher/Stats.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import useAuth from '@/hooks/useAuth'; +import { localFormat } from '@/utils/libs'; +import Skeleton from '@/components/skeleton/common/Skeleton'; + +type Props = { + campaignId?: string | string[]; + isTextHide?: boolean; +}; + +const AdImpressions = ({ campaignId, isTextHide }: Props) => { + const { data, loading } = useAuth('publisher/stats'); + const { data: campaignStats, loading: campaignDataLoading } = useAuth( + campaignId ? `campaign/${campaignId}/stats` : '', + ); + + const { data: campaignOverAllStats, loading: campaignOverAllDataLoading } = + useAuth(campaignId ? `campaign/${campaignId}/overall-stats` : ''); + + let totalImpression = +data?.totalImpression ? +data?.totalImpression : '0'; + let totalClicks = +data?.totalClicks ? +data?.totalClicks : '0'; + let totalAds = +data?.totalAds ? +data?.totalAds : '0'; + + return ( + <> + {!isTextHide ? ( +
+
+

+ {campaignId ? 'Active Subscription' : 'Total Campaigns'} + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignDataLoading ? ( + campaignStats?.activeSubscription.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }) + ) : ( + + ) + ) : ( + localFormat(totalAds.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Total Impressions + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignDataLoading ? ( + localFormat(campaignStats?.totalImpression) + ) : ( + + ) + ) : ( + localFormat(totalImpression.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Total Clicks +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignDataLoading ? ( + localFormat(campaignStats?.totalClicks) + ) : ( + + ) + ) : ( + localFormat(totalClicks.toString()) + )} +

+ ) : ( + + )} +
+
+ ) : ( +
+
+

+ {campaignId ? 'Active Subscription' : 'Total Campaigns'} + {!isTextHide ? ( +

[current Month]

+ ) : ( + !campaignId &&

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignOverAllDataLoading ? ( + campaignOverAllStats?.activeSubscription.toLocaleString( + 'en-US', + { + style: 'currency', + currency: 'USD', + }, + ) + ) : ( + + ) + ) : ( + localFormat(totalAds.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Total Impressions + {!isTextHide ? ( +

[current Month]

+ ) : ( +

[Overall]

+ )} +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignOverAllDataLoading ? ( + localFormat(campaignOverAllStats?.totalImpression) + ) : ( + + ) + ) : ( + localFormat(totalImpression.toString()) + )} +

+ ) : ( + + )} +
+
+

+ Total Clicks +

+ {!loading ? ( +

+ {campaignId ? ( + !campaignOverAllDataLoading ? ( + localFormat(campaignOverAllStats?.totalClicks) + ) : ( + + ) + ) : ( + localFormat(totalClicks.toString()) + )} +

+ ) : ( + + )} +
+
+ )} + + ); +}; + +export default AdImpressions; diff --git a/apps/app/src/components/Campaign/StartForm.tsx b/apps/app/src/components/Campaign/StartForm.tsx new file mode 100644 index 00000000..e936be6c --- /dev/null +++ b/apps/app/src/components/Campaign/StartForm.tsx @@ -0,0 +1,182 @@ +import React, { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useRouter } from 'next/router'; +import { request } from '@/hooks/useAuth'; +import Loader from '../skeleton/common/Loader'; +import { CampaignProps } from '@/utils/types'; + +const StartForm = ({ + campaignId, + campaignData, + mutate, + loading, + campaignMutate, +}: CampaignProps) => { + const router = useRouter(); + const [checked, setChecked] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubscriptionCancelled, setIsSubscriptionCancelled] = useState(false); + + useEffect(() => { + if (campaignData?.data?.subscription?.status === 'canceled') { + setIsSubscriptionCancelled(true); + } else { + setIsSubscriptionCancelled(false); + } + }, [campaignData?.data?.subscription?.status]); + + const onSubmit = async () => { + try { + if (!campaignId) { + if (!toast.isActive('add-campaign-info-error')) { + toast.error('Please add your campaign information first', { + toastId: 'add-campaign-info-error', + }); + } + setChecked((curr) => !curr); + } else { + setIsSubmitting((curr) => !curr); + await request.post( + `/campaign/${campaignId}/${ + campaignData && campaignData?.data?.is_active == 1 + ? 'stop' + : 'start' + }`, + ); + setIsSubmitting((curr) => !curr); + if (!toast.isActive('campaign-submit')) { + toast.success('Confirmation Submitted', { + toastId: 'campaign-submit', + }); + } + setChecked((curr) => !curr); + mutate(); + campaignMutate(); + router.push('/campaign'); + } + } catch (error: any) { + if (!toast.isActive('campaign-submit-error')) { + toast.error(error?.response?.data, { + toastId: 'campaign-submit-error', + }); + } + setIsSubmitting((curr) => !curr); + setChecked((curr) => !curr); + } + }; + + if (loading) { + return ( +
+
+ <> + + +
+ +
+ +
+
+
+
+ + +
+
+
+ +
+
+
+ ); + } + + return ( + <> +
+ {/*
*/} +
+

+ {!campaignData || campaignData?.data?.is_active == 0 + ? 'Start Campaign' + : 'Stop Campaign'} +

+
+
+

+ {!campaignData || campaignData?.data?.is_active == 0 + ? 'Your campaign will start once approved by an administrator' + : 'Are you sure you want to permanently stop this campaign?'} +

+ + {!campaignData || campaignData?.data?.is_active == 0 ? ( +
+
+ setChecked((curr) => !curr)} + checked={checked} + /> + + Confirm that I want to start this campaign. + +
+
+ +
+
+ ) : ( +
+
+ setChecked((curr) => !curr)} + checked={checked} + disabled={ + campaignData?.data?.subscription?.status === 'canceled' + } + /> + + Confirm that I want to stop this campaign. + +
+
+ +
+
+ )} +
+
+ + ); +}; +export default StartForm; diff --git a/apps/app/src/components/Campaign/TextAdForm.tsx b/apps/app/src/components/Campaign/TextAdForm.tsx new file mode 100644 index 00000000..fc9a4e67 --- /dev/null +++ b/apps/app/src/components/Campaign/TextAdForm.tsx @@ -0,0 +1,379 @@ +/* eslint-disable @next/next/no-img-element */ +import React, { useEffect, useState } from 'react'; +import { FormikValues, useFormik } from 'formik'; +import { toast, ToastContainer } from 'react-toastify'; +import StartForm from './StartForm'; +import PreviewAd from './PreviewAd'; +import Avatar from '../Icons/Avatar'; +import EmailCircle from '../Icons/EmailCircle'; +import { textCampaignValidation } from '@/utils/validationSchema'; +import { catchErrors } from '@/utils/libs'; +import { request } from '@/hooks/useAuth'; +import LoginCircle from '../Icons/LoginCircle'; +import { CampaignProps } from '@/utils/types'; +import Skeleton from '../skeleton/common/Skeleton'; + +const TextAdForm = ({ + campaignId, + campaignData, + mutate, + loading, + campaignMutate, +}: CampaignProps) => { + const validationSchema = textCampaignValidation(campaignData); + + const [isSubscriptionCancelled, setIsSubscriptionCancelled] = useState(false); + const [icon, setIcon] = useState(); + const [iconPreview, setIconPreview] = useState<{ icon?: string }>(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isOpen, setOpen] = useState(false); + + useEffect(() => { + if (campaignData?.data?.subscription?.status === 'canceled') { + setIsSubscriptionCancelled(true); + } else { + setIsSubscriptionCancelled(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [campaignData?.data?.subscription?.status]); + + useEffect(() => { + if (campaignId && campaignData) { + const iconUrl = campaignData?.data?.icon + ? `${campaignData?.data?.icon}` + : null; + if (iconUrl) { + formik.setFieldValue('icon', iconUrl); + setIconPreview({ + icon: iconUrl, + }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [campaignId, campaignData]); + + const onSubmit = async (values: FormikValues) => { + const formData = new FormData(); + formData.append('is_active', '0'); + try { + formData.append('text', values?.text); + formData.append('site_name', values?.site_name); + formData.append('link_name', values?.link_name); + formData.append('url', values?.url); + // Only append images if it's a File (indicating it has been changed) + if (values?.icon instanceof File) { + formData.append('icon', values?.icon); + } + setIsSubmitting(true); + await request.post(`/campaign/${campaignId}/text-ad`, formData); + // if (!toast.isActive('campaign-updated')) { + toast.success('Campaign Information updated', { + toastId: 'campaign-updated', + }); + // } + mutate(); + campaignMutate(); + setIsSubmitting(false); + } catch (error) { + const message = catchErrors(error); + // if (!toast.isActive('campaign-update-error')) { + toast.error(message, { + toastId: 'campaign-update-error', + }); + // } + setIsSubmitting(false); + } + setIsSubmitting(false); + }; + + const handleIconChange = (e: React.ChangeEvent) => { + formik.setFieldTouched('icon', true, false); + if (e.target.files && e.target.files[0]) { + const url = URL.createObjectURL(e.target.files[0]); + setIcon({ ...icon, icon: e.target.files[0] }); + setIconPreview({ ...iconPreview, icon: url }); + formik.setFieldValue('icon', e.target.files[0]); + } + }; + + const formik = useFormik({ + initialValues: { + url: campaignData?.data?.url || '', + text: campaignData?.data?.text || '', + site_name: campaignData?.data?.site_name || '', + link_name: campaignData?.data?.link_name || '', + icon: campaignData?.data?.icon || null, + }, + onSubmit, + validationSchema, + validateOnChange: true, + validateOnBlur: true, + enableReinitialize: true, + }); + + const handlePreview = () => { + setOpen((open) => !open); + }; + + return ( + <> + +
+
+

+ Campaign Information +

+
+

+ Edit the below campaign informations +

+
+
+
+

+ Placement +

+ {loading ? ( +
+ +
+ ) : ( +

+ {campaignData?.data?.title} +

+ )} +
+
+ {campaignData?.data?.start_date && ( +
+
+

+ Start Date +

+ {loading ? ( +
+ +
+ ) : ( +

+ {`${campaignData?.data?.start_date.slice(0, 19)} (UTC)`} +

+ )} +
+
+ )} +
+

+ URL +

+ {loading ? ( +
+ +
+ ) : ( +
+ + + {formik?.touched?.url && formik?.errors?.url && ( + + {formik?.errors?.url} + + )} +
+ )} +
+ +
+

+ Enter Site Name +

+ {loading ? ( +
+ +
+ ) : ( +
+ + {formik?.touched?.site_name && formik?.errors?.site_name && ( + + {formik?.errors?.site_name} + + )} +
+ )} +
+
+

+ Enter Link Text +

+ {loading ? ( +
+ +
+ ) : ( +
+ + {formik?.touched?.link_name && formik?.errors?.link_name && ( + + {formik?.errors?.link_name} + + )} +
+ )} +
+
+

+ Enter Display Text +

+ {loading ? ( +
+ +
+ ) : ( +
+