diff --git a/next-env.d.ts b/next-env.d.ts index fd36f949..4f11a03d 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js index 034a44ae..25811efc 100644 --- a/next.config.js +++ b/next.config.js @@ -1,13 +1,4 @@ const apiConfig = { - // Followed https://nextjs.org/docs/pages/building-your-application/routing/internationalization - i18n: { - // These are all the locales you want to support in - // your application - locales: ["en", "fr"], - // This is the default locale you want to be used when visiting - // a non-locale prefixed path e.g. `/hello` - defaultLocale: "en", - }, async rewrites() { return [ { diff --git a/package.json b/package.json index 16c02647..74234263 100644 --- a/package.json +++ b/package.json @@ -9,25 +9,30 @@ "scripts": { "dev": "next dev", "build": "next build", + "gen:geist": "ts-node -P scripts/tsconfig.json scripts/geist.ts", "gen:license": "ts-node -P scripts/tsconfig.json scripts/license.ts", "lint": "tsc --noEmit && prettier -w */**/*.{ts,tsx} && next lint", "start": "next start" }, "dependencies": { + "@formatjs/intl-localematcher": "^0.5.2", "@geist-ui/react": "^2.2.5", "@geist-ui/react-icons": "^1.0.1", "@hcaptcha/react-hcaptcha": "^1.9.2", "@reacherhq/api": "^0.3.10", "@sendinblue/client": "^3.3.1", - "@sentry/nextjs": "^7.86.0", + "@sentry/nextjs": "^7.91.0", "@stripe/stripe-js": "^1.52.0", - "@supabase/supabase-js": "^1.35.7", + "@supabase/auth-ui-react": "^0.4.6", + "@supabase/ssr": "^0.0.10", + "@supabase/supabase-js": "^2.39.1", "@types/amqplib": "^0.10.4", "@types/async-retry": "^1.4.8", "@types/cors": "^2.8.17", "@types/mailgun-js": "^0.22.18", "@types/markdown-pdf": "^9.0.5", "@types/mustache": "^4.2.5", + "@types/negotiator": "^0.6.3", "@types/react": "18.2.19", "@types/react-dom": "^18.2.17", "@types/request-ip": "^0.0.41", @@ -42,7 +47,8 @@ "markdown-pdf": "^11.0.0", "marked-react": "^2.0.0", "mustache": "^4.2.0", - "next": "13", + "negotiator": "^0.6.3", + "next": "^14.0.4", "rate-limiter-flexible": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/scripts/geist.ts b/scripts/geist.ts new file mode 100644 index 00000000..0365ce93 --- /dev/null +++ b/scripts/geist.ts @@ -0,0 +1,304 @@ +import { Themes } from "@geist-ui/react"; + +const theme = Themes.createFromLight({ + type: "default", + palette: { + errorDark: "#ff128a", // Accent Color Pink + foreground: "#3a3a3a", // Neutral Almost Black + success: "#6979f8", // Primary Blue + link: "#6979f8", + cyan: "#6979f8", + secondary: "#999999", // Neutral Light Gray + }, +}); + +const css = ` +html, +body { + background-color: ${theme.palette.background}; + color: ${theme.palette.foreground}; +} + +html { + font-size: 16px; + --geist-icons-background: ${theme.palette.background}; +} + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-size: 1rem; + line-height: 1.5; + margin: 0; + padding: 0; + min-height: 100%; + position: relative; + overflow-x: hidden; + font-family: ${theme.font.sans}; +} + +#__next { + overflow-x: hidden; +} + +*, +*:before, +*:after { + box-sizing: inherit; + text-rendering: geometricPrecision; + -webkit-tap-highlight-color: transparent; +} + +p, +small { + font-weight: 400; + color: inherit; + letter-spacing: -0.005625em; + font-family: ${theme.font.sans}; +} + +p { + margin: 1em 0; + font-size: 1em; + line-height: 1.625em; +} + +small { + margin: 0; + line-height: 1.5; + font-size: 0.875em; +} + +b { + font-weight: 600; +} + +span { + font-size: inherit; + color: inherit; + font-weight: inherit; +} + +img { + max-width: 100%; +} + +a { + cursor: pointer; + font-size: inherit; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-box-align: center; + align-items: center; + color: ${theme.palette.link}; + text-decoration: ${theme.expressiveness.linkStyle}; +} + +a:hover { + text-decoration: ${theme.expressiveness.linkHoverStyle}; +} + +ul, +ol { + padding: 0; + list-style-type: none; + margin: ${theme.layout.gapHalf} ${theme.layout.gapHalf} + ${theme.layout.gapHalf} ${theme.layout.gap}; + color: ${theme.palette.foreground}; +} + +ol { + list-style-type: decimal; +} + +li { + margin-bottom: 0.625em; + font-size: 1em; + line-height: 1.625em; +} + +ul li:before { + content: "–"; + display: inline-block; + color: ${theme.palette.accents_4}; + position: absolute; + margin-left: -0.9375em; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: inherit; + margin: 0 0 0.7rem 0; +} + +h1 { + font-size: 3rem; + letter-spacing: -0.02em; + line-height: 1.5; + font-weight: 700; +} + +h2 { + font-size: 2.25rem; + letter-spacing: -0.02em; + font-weight: 600; +} + +h3 { + font-size: 1.5rem; + letter-spacing: -0.02em; + font-weight: 600; +} + +h4 { + font-size: 1.25rem; + letter-spacing: -0.02em; + font-weight: 600; +} + +h5 { + font-size: 1rem; + letter-spacing: -0.01em; + font-weight: 600; +} + +h6 { + font-size: 0.875rem; + letter-spacing: -0.005em; + font-weight: 600; +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + margin: 0; +} + +button:focus, +input:focus, +select:focus, +textarea:focus { + outline: none; +} + +code { + color: ${theme.palette.code}; + font-family: ${theme.font.mono}; + font-size: 0.9em; + white-space: pre-wrap; +} + +code:before, +code:after { + content: "\`"; +} + +pre { + padding: calc(${theme.layout.gap} * 0.9) ${theme.layout.gap}; + margin: ${theme.layout.gap} 0; + border: 1px solid ${theme.palette.accents_2}; + border-radius: ${theme.layout.radius}; + font-family: ${theme.font.mono}; + white-space: pre; + overflow: auto; + line-height: 1.5; + text-align: left; + font-size: 14px; + -webkit-overflow-scrolling: touch; +} + +pre code { + color: ${theme.palette.foreground}; + font-size: 1em; + line-height: 1.25em; + white-space: pre; +} + +pre code:before, +pre code:after { + display: none; +} + +pre :global(p) { + margin: 0; +} + +pre::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + background: transparent; +} + +hr { + border-color: ${theme.palette.accents_2}; +} + +details { + background-color: ${theme.palette.accents_1}; + border: none; +} + +details:focus, +details:hover, +details:active { + outline: none; +} + +summary { + cursor: pointer; + user-select: none; + list-style: none; + outline: none; +} + +summary::marker, +summary::before, +summary::-webkit-details-marker { + display: none; +} + +summary::-moz-list-bullet { + font-size: 0; +} + +summary:focus, +summary:hover, +summary:active { + outline: none; + list-style: none; +} + +blockquote { + padding: calc(0.667 * ${theme.layout.gap}) ${theme.layout.gap}; + color: ${theme.palette.accents_5}; + background-color: ${theme.palette.accents_1}; + border-radius: ${theme.layout.radius}; + margin: 1.5em 0; + border: 1px solid ${theme.palette.border}; +} + +blockquote :global(*:first-child) { + margin-top: 0; +} + +blockquote :global(*:last-child) { + margin-bottom: 0; +} + +::selection { + background-color: ${theme.palette.selection}; + color: ${theme.palette.foreground}; +}`; + +console.log(css); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 9c1e04e7..968a620c 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "../node_modules/@amaurym/tsconfig/tsconfig.json" + "compilerOptions": { + "esModuleInterop": true, + "strict": true + } } diff --git a/src/components/Dashboard/ApiUsage.module.css b/src/app/[lang]/dashboard/ApiUsage.module.css similarity index 100% rename from src/components/Dashboard/ApiUsage.module.css rename to src/app/[lang]/dashboard/ApiUsage.module.css diff --git a/src/app/[lang]/dashboard/ApiUsage.tsx b/src/app/[lang]/dashboard/ApiUsage.tsx new file mode 100644 index 00000000..42810a8e --- /dev/null +++ b/src/app/[lang]/dashboard/ApiUsage.tsx @@ -0,0 +1,113 @@ +import { Capacity, Text } from "@geist-ui/react"; +import { Loader } from "@geist-ui/react-icons"; +import React, { useEffect, useState } from "react"; +import { sentryException } from "@/util/sentry"; +import { subApiMaxCalls } from "@/util/subs"; +import styles from "./ApiUsage.module.css"; +import { formatDate } from "@/util/helpers"; +import { Dictionary } from "@/dictionaries"; +import { Tables } from "@/supabase/database.types"; +import { SupabaseClient } from "@supabase/supabase-js"; +import { parseISO, subMonths } from "date-fns"; +import { SubscriptionWithPrice } from "@/supabase/supabaseServer"; +import { createClient } from "@/supabase/client"; + +interface ApiUsageProps { + d: Dictionary; + subscription: SubscriptionWithPrice; +} + +export function ApiUsage({ + subscription, + d, +}: ApiUsageProps): React.ReactElement { + const supabase = createClient(); + const [apiCalls, setApiCalls] = useState(undefined); // undefined means loading + + useEffect(() => { + const t = setInterval(() => { + getApiUsageClient(supabase, subscription) + .then(setApiCalls) + .catch(sentryException); + }, 3000); + + return () => clearInterval(t); + }, [supabase, subscription]); + + return ( +
+
+ + {d.dashboard.emails_this_month} + {subscription && ( + <> + {" "} + ( + {formatDate( + subscription.current_period_start, + d.lang + )}{" "} + -{" "} + {formatDate( + subscription.current_period_end, + d.lang + )} + ) + + )} + + + + + {apiCalls === undefined ? ( + + ) : ( + apiCalls + )} + + /{subApiMaxCalls(subscription?.prices?.product_id)} + +
+ + +
+ ); +} + +// Get the api calls of a user in the past month. +async function getApiUsageClient( + supabase: SupabaseClient, + subscription: Tables<"subscriptions"> | null +): Promise { + const { error, count } = await supabase + .from("calls") + .select("*", { count: "exact" }) + .gt("created_at", getUsageStartDate(subscription).toISOString()); + + if (error) { + throw error; + } + + return count || 0; +} + +// Returns the start date of the usage metering. +// - If the user has an active subscription, it's the current period's start +// date. +// - If not, then it's 1 month rolling. +function getUsageStartDate(subscription: Tables<"subscriptions"> | null): Date { + if (!subscription) { + return subMonths(new Date(), 1); + } + + return typeof subscription.current_period_start === "string" + ? parseISO(subscription.current_period_start) + : subscription.current_period_start; +} diff --git a/src/app/[lang]/dashboard/Dashboard.tsx b/src/app/[lang]/dashboard/Dashboard.tsx new file mode 100644 index 00000000..a234e66c --- /dev/null +++ b/src/app/[lang]/dashboard/Dashboard.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Page, Spacer } from "@geist-ui/react"; +import React from "react"; +import { ApiUsage } from "./ApiUsage"; +import { Tabs, TabsProps } from "./Tabs"; +import { SAAS_10K_PRODUCT_ID } from "@/util/subs"; +import { SubscriptionHeader } from "./SubscriptionHeader"; +import { SubscriptionWithPrice } from "@/supabase/supabaseServer"; +import { Dictionary } from "@/dictionaries"; + +interface DashboardProps { + children: React.ReactNode; + d: Dictionary; + showApiUsage?: boolean; + subscription: SubscriptionWithPrice | null; + tab: TabsProps["tab"] | false; +} +export function Dashboard({ + children, + d, + showApiUsage, + subscription, + tab, +}: DashboardProps) { + return ( + + + + {showApiUsage && subscription && ( + <> + + + + )} + {tab !== false && ( + + )} + {children} + + ); +} diff --git a/src/components/Dashboard/StripeManageButton.tsx b/src/app/[lang]/dashboard/StripeManageButton.tsx similarity index 77% rename from src/components/Dashboard/StripeManageButton.tsx rename to src/app/[lang]/dashboard/StripeManageButton.tsx index 322636fa..c3bcf911 100644 --- a/src/components/Dashboard/StripeManageButton.tsx +++ b/src/app/[lang]/dashboard/StripeManageButton.tsx @@ -3,32 +3,26 @@ import React, { useState } from "react"; import { postData } from "@/util/helpers"; import { sentryException } from "@/util/sentry"; -import { useUser } from "@/util/useUser"; -import { useRouter } from "next/router"; +import { Dictionary } from "@/dictionaries"; export interface StripeMananageButton { children: React.ReactNode | string; + d: Dictionary; } export function StripeMananageButton({ children, + d, }: StripeMananageButton): React.ReactElement { const [loading, setLoading] = useState(false); - const { session } = useUser(); - const router = useRouter(); const redirectToCustomerPortal = async () => { setLoading(true); try { - if (!session?.access_token) { - throw new Error("session access_token is empty"); - } - const { url } = await postData<{ url: string }>({ url: "/api/stripe/create-portal-link", - token: session.access_token, data: { - locale: router.locale, + locale: d.lang, }, }); diff --git a/src/components/Dashboard/SubscriptionHeader.module.css b/src/app/[lang]/dashboard/SubscriptionHeader.module.css similarity index 100% rename from src/components/Dashboard/SubscriptionHeader.module.css rename to src/app/[lang]/dashboard/SubscriptionHeader.module.css diff --git a/src/components/Dashboard/SubscriptionHeader.tsx b/src/app/[lang]/dashboard/SubscriptionHeader.tsx similarity index 60% rename from src/components/Dashboard/SubscriptionHeader.tsx rename to src/app/[lang]/dashboard/SubscriptionHeader.tsx index 8a12fa8e..5e11a48e 100644 --- a/src/components/Dashboard/SubscriptionHeader.tsx +++ b/src/app/[lang]/dashboard/SubscriptionHeader.tsx @@ -1,25 +1,27 @@ import { Text } from "@geist-ui/react"; import React from "react"; -import Link from "next/link"; import { StripeMananageButton } from "./StripeManageButton"; -import { COMMERCIAL_LICENSE_PRODUCT_ID, productName } from "@/util/subs"; +import { + COMMERCIAL_LICENSE_PRODUCT_ID, + SAAS_100K_PRODUCT_ID, + SAAS_10K_PRODUCT_ID, +} from "@/util/subs"; import { formatDate } from "@/util/helpers"; -import { useRouter } from "next/router"; -import { dictionary } from "@/dictionaries"; -import { SubscriptionWithPrice } from "@/supabase/domain.types"; +import { Dictionary } from "@/dictionaries"; import styles from "./SubscriptionHeader.module.css"; +import { SubscriptionWithPrice } from "@/supabase/supabaseServer"; +import { DLink } from "@/components/DLink"; interface SubscriptionHeaderProps { + d: Dictionary; subscription: SubscriptionWithPrice | null; } export function SubscriptionHeader({ + d, subscription, }: SubscriptionHeaderProps): React.ReactElement { - const router = useRouter(); - const d = dictionary(router.locale); - return (
@@ -30,7 +32,7 @@ export function SubscriptionHeader({ ? d.dashboard.header.thanks_for_subscription.replace( "%s", productName( - subscription?.prices?.products, + subscription?.prices?.product_id, d ) ) @@ -39,12 +41,12 @@ export function SubscriptionHeader({ {subscription && ( <> - + {d.dashboard.header.manage_subscription} )} - + {d.dashboard.header.billing_history}
@@ -53,31 +55,46 @@ export function SubscriptionHeader({ {d.dashboard.header.active_subscription} - {productName(subscription?.prices?.products, d)} + {productName(subscription?.prices?.product_id, d)} {subscription?.status === "active" && subscription?.cancel_at && ( {d.dashboard.header.plan_ends_on.replace( "%s", - formatDate( - new Date(subscription.cancel_at), - router.locale - ) + formatDate(new Date(subscription.cancel_at), d.lang) )} )} - {subscription?.prices?.products?.id !== + {subscription?.prices?.product_id !== COMMERCIAL_LICENSE_PRODUCT_ID && (
- {d.dashboard.header.upgrade} - +
)}
); } + +// Get the user-friendly name of a product. +function productName( + product_id: string | null | undefined, + d: Dictionary +): string { + switch (product_id) { + case COMMERCIAL_LICENSE_PRODUCT_ID: + return d.pricing.plans.commercial_license; + case SAAS_100K_PRODUCT_ID: + return d.pricing.plans.saas_100k; + case SAAS_10K_PRODUCT_ID: + return d.pricing.plans.saas_10k; + default: + return d.dashboard.header.no_active_subscription; + } +} diff --git a/src/app/[lang]/dashboard/Tabs.tsx b/src/app/[lang]/dashboard/Tabs.tsx new file mode 100644 index 00000000..4d03557b --- /dev/null +++ b/src/app/[lang]/dashboard/Tabs.tsx @@ -0,0 +1,54 @@ +import { Tabs as GTabs } from "@geist-ui/react"; +import React from "react"; +import { Dictionary } from "@/dictionaries"; +import Mail from "@geist-ui/react-icons/mail"; +import Database from "@geist-ui/react-icons/database"; +import Lock from "@geist-ui/react-icons/lock"; +import { ENABLE_BULK } from "@/util/helpers"; +import { useRouter } from "next/navigation"; + +export interface TabsProps { + d: Dictionary; + bulkDisabled: boolean; + tab: "verify" | "bulk" | "api"; +} + +export function Tabs({ bulkDisabled, tab, ...props }: TabsProps) { + const router = useRouter(); + const d = props.d.dashboard.tabs; + + const handler = (value: string) => { + router.push(`/${props.d.lang}/dashboard/${value}`); + }; + + return ENABLE_BULK === 1 ? ( + + + + {d.verify} + + } + value="verify" + /> + + + {d.bulk} + + ) : ( + <> + + {d.bulk} + + ) + } + value="bulk" + /> + + ) : null; +} diff --git a/src/components/Dashboard/BulkHistory.tsx b/src/app/[lang]/dashboard/bulk/BulkHistory.tsx similarity index 85% rename from src/components/Dashboard/BulkHistory.tsx rename to src/app/[lang]/dashboard/bulk/BulkHistory.tsx index 53a03b5a..f07059a6 100644 --- a/src/components/Dashboard/BulkHistory.tsx +++ b/src/app/[lang]/dashboard/bulk/BulkHistory.tsx @@ -1,34 +1,24 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { dictionary } from "@/dictionaries"; +import { Dictionary } from "@/dictionaries"; import { Button, Card, Spacer, Table, Text } from "@geist-ui/react"; -import { supabase } from "@/util/supabaseClient"; import { Tables } from "@/supabase/database.types"; import { sentryException } from "@/util/sentry"; import { formatDate } from "@/util/helpers"; import { Download } from "@geist-ui/react-icons"; import { TableColumnRender } from "@geist-ui/react/esm/table"; import Check from "@geist-ui/react-icons/check"; +import { createClient } from "@/supabase/client"; -export function alertError( - e: string, - d: ReturnType["dashboard"]["get_started_saas"] -) { - alert(d.unexpected_error.replace("%s2", e)); -} - -export function BulkHistory(): React.ReactElement { +export function BulkHistory(props: { d: Dictionary }) { const [bulkJobs, setBulkJobs] = useState< Tables<"bulk_jobs_info">[] | undefined >(); - const router = useRouter(); - const d = dictionary(router.locale).dashboard.get_started_bulk.history; + const supabase = createClient(); + const d = props.d.dashboard.get_started_bulk.history; useEffect(() => { setInterval(async () => { - const res = await supabase - .from>("bulk_jobs_info") - .select("*"); + const res = await supabase.from("bulk_jobs_info").select("*"); if (res.error) { sentryException(res.error); return; @@ -36,7 +26,7 @@ export function BulkHistory(): React.ReactElement { setBulkJobs(res.data); }, 3000); - }, []); + }, [supabase]); const renderStatus: TableColumnRender> = ( _value, @@ -97,7 +87,7 @@ export function BulkHistory(): React.ReactElement { prop="created_at" label={d.table.uploaded_at} render={(value) => ( - <>{formatDate(value as string, router.locale)} + <>{formatDate(value as string, props.d.lang)} )} /> ([]); const [loading, setLoading] = useState(false); @@ -110,21 +112,9 @@ export function GetStartedBulk(): React.ReactElement { if (!emails) { return; } - if (!userDetails) { - setUpload( - - ); - return; - } setLoading(true); postData({ url: `/api/v1/bulk`, - token: userDetails?.api_token, data: { input_type: "array", input: emails, @@ -189,7 +179,7 @@ export function GetStartedBulk(): React.ReactElement { - + ); } @@ -197,7 +187,7 @@ export function GetStartedBulk(): React.ReactElement { function UploadButton({ d, }: { - d: ReturnType["dashboard"]["get_started_bulk"]; + d: Dictionary["dashboard"]["get_started_bulk"]; }) { return ( <> @@ -215,7 +205,7 @@ function Analyzing({ file, }: { file: File; - d: ReturnType["dashboard"]["get_started_bulk"]; + d: Dictionary["dashboard"]["get_started_bulk"]; }) { return

{d.step_analzying.replace("%s", file.name)}

; } @@ -227,7 +217,7 @@ function Analyzed({ }: { file: File; emails: string[]; - d: ReturnType["dashboard"]["get_started_bulk"]; + d: Dictionary["dashboard"]["get_started_bulk"]; }) { const rows = emails.map((email) => ({ email })).slice(0, 3); if (emails.length > 3) { @@ -261,7 +251,7 @@ function Uploaded({ d, }: { emailsNum: number; - d: ReturnType["dashboard"]["get_started_bulk"]; + d: Dictionary["dashboard"]["get_started_bulk"]; }) { return ( <> @@ -282,7 +272,7 @@ function Error({ d, }: { error: string; - d: ReturnType["dashboard"]["get_started_bulk"]; + d: Dictionary["dashboard"]["get_started_bulk"]; }) { return ( <> diff --git a/src/app/[lang]/dashboard/bulk/page.tsx b/src/app/[lang]/dashboard/bulk/page.tsx new file mode 100644 index 00000000..104faacb --- /dev/null +++ b/src/app/[lang]/dashboard/bulk/page.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Dashboard } from "../Dashboard"; +import { ENABLE_BULK } from "@/util/helpers"; +import { getSubscription } from "@/supabase/supabaseServer"; +import { dictionary } from "@/dictionaries"; +import { GetStartedBulk } from "./GetStartedBulk"; + +export default async function Bulk({ + params: { lang }, +}: { + params: { lang: string }; +}) { + const subscription = await getSubscription(); + const d = await dictionary(lang); + + if (ENABLE_BULK === 0) { + return

Bulk is disabled

; + } + + return ( + + + + ); +} diff --git a/src/app/[lang]/dashboard/commercial_license/GetStartedCommercial.tsx b/src/app/[lang]/dashboard/commercial_license/GetStartedCommercial.tsx new file mode 100644 index 00000000..392c6a42 --- /dev/null +++ b/src/app/[lang]/dashboard/commercial_license/GetStartedCommercial.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { Dictionary } from "@/dictionaries"; +import { Card, Text } from "@/components/Geist"; +import Markdown from "marked-react"; +import React from "react"; + +export function GetStartedCommercial(props: { d: Dictionary }) { + const d = props.d.dashboard.get_started_license; + + return ( + + {d.title} + + {d.explanation} + + ); +} diff --git a/src/app/[lang]/dashboard/commercial_license/page.tsx b/src/app/[lang]/dashboard/commercial_license/page.tsx new file mode 100644 index 00000000..7b3ca69c --- /dev/null +++ b/src/app/[lang]/dashboard/commercial_license/page.tsx @@ -0,0 +1,31 @@ +import { dictionary } from "@/dictionaries"; +import React from "react"; +import { GetStartedCommercial } from "./GetStartedCommercial"; +import { Dashboard } from "../Dashboard"; +import { getSession, getSubscription } from "@/supabase/supabaseServer"; +import { redirect } from "next/navigation"; + +export default async function CommercialLicensePage({ + params: { lang }, +}: { + params: { lang: string }; +}) { + const session = await getSession(); + if (!session) { + return redirect(`/${lang}/login`); + } + + const subscription = await getSubscription(); + const d = await dictionary(lang); + + return ( + + + + ); +} diff --git a/src/app/[lang]/dashboard/layout.tsx b/src/app/[lang]/dashboard/layout.tsx new file mode 100644 index 00000000..8a4a123b --- /dev/null +++ b/src/app/[lang]/dashboard/layout.tsx @@ -0,0 +1,26 @@ +import { Footer } from "@/components/Footer"; +import { Nav } from "@/components/Nav/Nav"; +import { dictionary } from "@/dictionaries"; +import React from "react"; + +export const metadata = { + title: "Dashboard", +}; + +export default async function Layout({ + children, + params: { lang }, +}: { + children: React.ReactNode; + params: { lang: string }; +}) { + const d = await dictionary(lang); + + return ( + <> +