diff --git a/apps/backend/open-api/swagger.json b/apps/backend/open-api/swagger.json index c532b5bf..6a19cdab 100644 --- a/apps/backend/open-api/swagger.json +++ b/apps/backend/open-api/swagger.json @@ -1582,8 +1582,7 @@ "userId", "organisationId", "createdDate", - "updateDate", - "deletedDate" + "updateDate" ] }, "UserApiKey": { @@ -1624,8 +1623,7 @@ "description", "userId", "createdDate", - "updateDate", - "deletedDate" + "updateDate" ] }, "UserDto": { @@ -1702,8 +1700,7 @@ "memberships", "apiKeys", "createdDate", - "updateDate", - "deletedDate" + "updateDate" ] }, "UpdateUserDto": { @@ -1778,8 +1775,7 @@ "uuid", "name", "createdDate", - "updateDate", - "deletedDate" + "updateDate" ] }, "User": { @@ -1850,8 +1846,7 @@ "emailVerified", "blocked", "createdDate", - "updateDate", - "deletedDate" + "updateDate" ] }, "UpdateOrganisationDto": { @@ -2080,8 +2075,7 @@ "validUntil", "organisationId", "createdDate", - "updatedDate", - "deletedDate" + "updatedDate" ] }, "SaveOrganisationSubscriptionRecordDto": { diff --git a/apps/frontend/src/account/NoSubscriptions.tsx b/apps/frontend/src/account/NoSubscriptions.tsx index c4a54e71..632f0f6e 100644 --- a/apps/frontend/src/account/NoSubscriptions.tsx +++ b/apps/frontend/src/account/NoSubscriptions.tsx @@ -33,7 +33,7 @@ export default function NoSubscriptions({ You have no subscriptions.

- Get started with Miller by subscribing. + Get started by subscribing.

diff --git a/apps/frontend/src/layout/Header.tsx b/apps/frontend/src/layout/Header.tsx index 5964339d..c3c6893a 100644 --- a/apps/frontend/src/layout/Header.tsx +++ b/apps/frontend/src/layout/Header.tsx @@ -1,4 +1,4 @@ -import { Fragment, PropsWithChildren, useState } from "react"; +import { Fragment, PropsWithChildren } from "react"; import { NavLink } from "react-router-dom"; import { Popover, Transition } from "@headlessui/react"; import clsx from "clsx"; diff --git a/apps/marketing/src/components/Header.tsx b/apps/marketing/src/components/Header.tsx index 66fe68d0..86f585ec 100644 --- a/apps/marketing/src/components/Header.tsx +++ b/apps/marketing/src/components/Header.tsx @@ -96,10 +96,9 @@ export const MobileNavigation = () => { Features Pricing + Features {user && ( - + Dashboard )} @@ -170,11 +169,7 @@ export function Header() { Sign In )} {user && ( - - Dashboard - + Dashboard )}
diff --git a/apps/marketing/src/docs/components/LeftMenu.tsx b/apps/marketing/src/components/LeftMenu.tsx similarity index 79% rename from apps/marketing/src/docs/components/LeftMenu.tsx rename to apps/marketing/src/components/LeftMenu.tsx index cb6f3267..6472da9a 100644 --- a/apps/marketing/src/docs/components/LeftMenu.tsx +++ b/apps/marketing/src/components/LeftMenu.tsx @@ -1,5 +1,6 @@ import { colorVariants } from "@use-miller/shared-frontend-tooling"; import clsx from "clsx"; +import Link from "next/link.js"; import { useRouter } from "next/router.js"; import { LeftMenuItem } from "./LeftMenuItem.js"; @@ -24,23 +25,36 @@ const isCurrentMenuItem = (path: string, item: MenuItem) => { return item.path === path; }; -export function LeftMenu({ menuSections }: { menuSections: MenuSection[] }) { +export function LeftMenu({ + menuSections, + header, + headerHref, +}: { + menuSections: MenuSection[]; + header: string; + headerHref?: string; +}) { const path = useRouter(); return (
-
+

- Docs + + {header} +

{menuSections?.map((section) => (
-

+

{section.name}

    diff --git a/apps/marketing/src/docs/components/LeftMenuItem.tsx b/apps/marketing/src/components/LeftMenuItem.tsx similarity index 100% rename from apps/marketing/src/docs/components/LeftMenuItem.tsx rename to apps/marketing/src/components/LeftMenuItem.tsx diff --git a/apps/marketing/src/components/LeftMenuWrappedContent.tsx b/apps/marketing/src/components/LeftMenuWrappedContent.tsx new file mode 100644 index 00000000..bf804501 --- /dev/null +++ b/apps/marketing/src/components/LeftMenuWrappedContent.tsx @@ -0,0 +1,31 @@ +import { PropsWithChildren } from "react"; +import { LeftMenu, MenuSection } from "./LeftMenu.jsx"; +import { Container } from "./Container.jsx"; +import Layout from "./Layout.jsx"; + +export const LeftMenuWrappedContent = ({ + menuSections, + menuHeaderTitle, + menuHeaderHref, + children, +}: { + menuSections: MenuSection[]; + + menuHeaderTitle: string; + menuHeaderHref: string; +} & PropsWithChildren) => { + return ( + + +
    + + {children} +
    +
    +
    + ); +}; diff --git a/apps/marketing/src/dashboard/components/DashboardDetails.tsx b/apps/marketing/src/dashboard/components/DashboardDetails.tsx new file mode 100644 index 00000000..9d159182 --- /dev/null +++ b/apps/marketing/src/dashboard/components/DashboardDetails.tsx @@ -0,0 +1,23 @@ +import { OrganisationSubscriptionRecord } from "@use-miller/shared-api-client"; +import NoSubscriptions from "./NoSubscriptions.jsx"; +import { Subscriptions } from "./Subscriptions.jsx"; + +export const DashboardDetails = ({ + title, + subs, +}: { + title: string; + subs: OrganisationSubscriptionRecord[]; +}) => { + let subsComponent = ; + if (subs.length === 0) { + subsComponent = ; + } + + return ( +
    +

    {title}

    +
    {subsComponent}
    +
    + ); +}; diff --git a/apps/marketing/src/dashboard/components/ManageBillingLink.tsx b/apps/marketing/src/dashboard/components/ManageBillingLink.tsx new file mode 100644 index 00000000..fa41cf0b --- /dev/null +++ b/apps/marketing/src/dashboard/components/ManageBillingLink.tsx @@ -0,0 +1,31 @@ +import { StyledButton } from "@use-miller/shared-frontend-tooling"; +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; +import { useGetCustomerPortalSession } from "../../hooks/useGetCustomerPortalSession.js"; +const ManageBillingLink = ({ + subscriptionUuid, + paymentProvider, +}: { + subscriptionUuid: string; + paymentProvider: string; +}) => { + const { mutateAsync } = useGetCustomerPortalSession(); + let linkClick = async (uuid: string) => { + const url = new URL(window.location.href); + + const link = await mutateAsync({ + returnUrl: url.pathname, + subscriptionRecordUuid: uuid, + }); + // redirect to it + window.location.href = link.sessionUrl; + }; + + return ( + linkClick(subscriptionUuid)}> + Manage Billing {paymentProvider ? "on " + paymentProvider : ""} + + + ); +}; + +export default ManageBillingLink; diff --git a/apps/marketing/src/dashboard/components/NoSubscriptions.tsx b/apps/marketing/src/dashboard/components/NoSubscriptions.tsx new file mode 100644 index 00000000..f7afa094 --- /dev/null +++ b/apps/marketing/src/dashboard/components/NoSubscriptions.tsx @@ -0,0 +1,53 @@ +import { ShoppingBagIcon } from "@heroicons/react/24/outline"; +import { + colorVariants, + StyledButton, +} from "@use-miller/shared-frontend-tooling"; +import { useGetPaymentLink } from "../../hooks/useGetPaymentLink.js"; + +export default function NoSubscriptions({ + organisationUuid, +}: { + organisationUuid?: string; +}) { + const { mutateAsync } = useGetPaymentLink(); + + const onClick = async () => { + const link = await mutateAsync({ + successFrontendPath: "/dashboard", + cancelFrontendPath: "/dashboard", + lineItems: [ + { + price: process.env.NEXT_PUBLIC_STRIPE_REGULAR_PRICE_ID, + quantity: 1, + }, + ], + mode: "subscription", + organisationId: organisationUuid, + }); + + window.location.href = link.stripeSessionUrl; + }; + const colorVariant = "green"; + return ( +
    +

    + You have no subscriptions. +

    +

    + Get started by subscribing. +

    +
    + + +
    +
    + ); +} diff --git a/apps/marketing/src/dashboard/components/ProfileDetails.tsx b/apps/marketing/src/dashboard/components/ProfileDetails.tsx new file mode 100644 index 00000000..0279a53a --- /dev/null +++ b/apps/marketing/src/dashboard/components/ProfileDetails.tsx @@ -0,0 +1,23 @@ +import { User } from "@use-miller/shared-api-client"; +import { useFormattedDate } from "../../hooks/useFormattedDate.js"; + +export const ProfileDetails = ({ currentUser }: { currentUser: User }) => { + const formattedDate = useFormattedDate(currentUser?.createdDate); + return ( +
    +

    Profile

    +

    Manage your profile

    +
    +
    +
    +

    Email

    +

    {currentUser.email}

    +
    +
    +

    Member since

    +

    {formattedDate}

    +
    +
    +
    + ); +}; diff --git a/apps/marketing/src/dashboard/components/SingleSubscription.tsx b/apps/marketing/src/dashboard/components/SingleSubscription.tsx new file mode 100644 index 00000000..31ab2017 --- /dev/null +++ b/apps/marketing/src/dashboard/components/SingleSubscription.tsx @@ -0,0 +1,53 @@ +import { OrganisationSubscriptionRecord } from "@use-miller/shared-api-client"; +import { colorVariants } from "@use-miller/shared-frontend-tooling"; +import { useFormattedDate } from "../../hooks/useFormattedDate.js"; +import ManageBillingLink from "./ManageBillingLink.jsx"; + +export const SingleSubscription = ({ + subscriptionRecord, +}: { + subscriptionRecord: OrganisationSubscriptionRecord; +}) => { + const expiryDate = useFormattedDate(subscriptionRecord.validUntil); + const createdDate = useFormattedDate(subscriptionRecord.createdDate); + const colorVariant = "green"; + return ( +
    +
    +

    + {subscriptionRecord.productDisplayName} +

    +
    + {subscriptionRecord.paymentSystemMode === + "subscription" && ( +
    +

    + Valid Until +

    +

    {expiryDate}

    +
    + )} +
    +

    Started

    +

    {createdDate}

    +
    +
    +
    +

    + Last Transaction Id +

    +

    + {subscriptionRecord.paymentSystemTransactionId} +

    +
    + + +
    +
    + ); +}; diff --git a/apps/marketing/src/dashboard/components/Subscriptions.tsx b/apps/marketing/src/dashboard/components/Subscriptions.tsx new file mode 100644 index 00000000..583885ff --- /dev/null +++ b/apps/marketing/src/dashboard/components/Subscriptions.tsx @@ -0,0 +1,27 @@ +import { OrganisationSubscriptionRecord } from "@use-miller/shared-api-client"; +import { SingleSubscription } from "./SingleSubscription.jsx"; + +export const Subscriptions = ({ + subs, +}: { + subs: OrganisationSubscriptionRecord[]; +}) => { + return ( +
    +

    + Your Subscriptions +

    + +
    + {subs.map((sub) => { + return ( + + ); + })} +
    +
    + ); +}; diff --git a/apps/marketing/src/dashboard/dashboardDataService.ts b/apps/marketing/src/dashboard/dashboardDataService.ts new file mode 100644 index 00000000..4f7a7c92 --- /dev/null +++ b/apps/marketing/src/dashboard/dashboardDataService.ts @@ -0,0 +1,137 @@ +import { getAccessToken } from "@auth0/nextjs-auth0"; +import { + OrganisationsApi, + OrganisationSubscriptionRecord, + OrganisationSubscriptionsApi, + UsersApi, +} from "@use-miller/shared-api-client"; +import { getAuthenticatedApiInstance } from "@use-miller/shared-frontend-tooling"; +import { GetServerSidePropsContext, PreviewData } from "next"; +import { ParsedUrlQuery } from "querystring"; +import { createMenu } from "./leftMenuGeneration.js"; + +export const getUserOrgs = async (accessToken: string) => { + const apiClient = await getAuthenticatedApiInstance( + OrganisationsApi, + process.env.NEXT_PUBLIC_API_BASE_PATH, + accessToken, + fetch + ); + const userData = await apiClient.organisationControllerFindAllForUser(); + return userData; +}; + +export const getSubscriptions = async (accessToken: string, orgId: number) => { + const apiClient = await getAuthenticatedApiInstance( + OrganisationSubscriptionsApi, + process.env.NEXT_PUBLIC_API_BASE_PATH, + accessToken, + fetch + ); + const data = await apiClient.organisationSubscriptionsControllerFindAll({ + orgId, + }); + return data; +}; + +export async function dashboardGetSspData( + context: GetServerSidePropsContext +) { + const atResponse = await getAccessToken(context.req, context.res, { + scopes: ["openid", "email", "profile", "offline_access"], + }); + const orgUuid = context.params?.orgUuid + ? (context.params?.orgUuid as string) + : undefined; + const data = await getIndexDashboardData( + atResponse.accessToken!, // user can't be logged in + orgUuid + ); + return { + props: data, + }; +} + +export const getIndexDashboardData = async ( + accessToken: string, + currentOrg?: string +) => { + const userOrgs = await getUserOrgs(accessToken); + // org data permissions are enforced on the server + // so we can just return the data + const currentOrgInstance = currentOrg + ? userOrgs.find((org) => org.uuid === currentOrg) + : userOrgs[0]; + if (!currentOrgInstance) { + throw new Error("No data for found for this user"); + } + const [subscriptions, menuSections] = await Promise.allSettled([ + getSubscriptions(accessToken, currentOrgInstance.id), + createMenu(userOrgs), + ]); + if ( + subscriptions.status === "rejected" || + menuSections.status === "rejected" + ) { + throw new Error("Unable to get account data"); + } + const oneYearsTime = new Date(); + oneYearsTime.setFullYear(oneYearsTime.getFullYear() + 1); + const fakeSubs = [ + { + id: 1, + uuid: "payment-uuid", + productDisplayName: "Miller Web", + paymentSystemTransactionId: "payment-system-transaction-id", + paymentSystemProductId: "payment-system-product-id", + paymentSystemCustomerId: "payment-system-customer-id", + paymentSystemCustomerEmail: "payment-system-customer-email", + paymentSystemMode: "subscription", + paymentSystemName: "stripe", + validUntil: oneYearsTime, + organisationId: 1, + createdDate: new Date(), + updatedDate: new Date(), + deletedDate: undefined, + } as OrganisationSubscriptionRecord, + ]; + return JSON.parse( + JSON.stringify({ + menuSections: menuSections.value, + subs: subscriptions.value, + // subs: fakeSubs, + currentOrg: currentOrgInstance, + }) + ); +}; + +export const getUserData = async (accessToken: string) => { + const apiClient = await getAuthenticatedApiInstance( + UsersApi, + process.env.NEXT_PUBLIC_API_BASE_PATH, + accessToken, + fetch + ); + const userData = await apiClient.userControllerFindOne({ + uuid: "me", + }); + return userData; +}; + +export const getAccountIndexData = async (accessToken: string) => { + const userOrgs = await getUserOrgs(accessToken); + // org data permissions are enforced on the server + // so we can just return the data + + const [userData, menuData] = await Promise.allSettled([ + getUserData(accessToken), + createMenu(userOrgs), + ]); + if (userData.status === "rejected" || menuData.status === "rejected") { + throw new Error("Unable to get account data"); + } + return { + menuSections: menuData.value, + currentUser: JSON.parse(JSON.stringify(userData.value)), + }; +}; diff --git a/apps/marketing/src/dashboard/leftMenuGeneration.ts b/apps/marketing/src/dashboard/leftMenuGeneration.ts new file mode 100644 index 00000000..6ac79d47 --- /dev/null +++ b/apps/marketing/src/dashboard/leftMenuGeneration.ts @@ -0,0 +1,31 @@ +import { Organisation } from "@use-miller/shared-api-client"; + +export const createMenu = (userOrgs: Organisation[]) => { + const menuSections = []; + + menuSections.push({ + name: "Organisations", + slug: "organisations", + items: userOrgs.map((org) => ({ + name: org.name, + path: `/dashboard/${org.uuid}`, + })), + }); + + menuSections.push({ + name: "Account", + slug: "account", + items: [ + { + name: "Profile", + path: "/dashboard/account/profile", + }, + { + name: "Sign Out", + path: "/api/auth/logout", + }, + ], + }); + + return menuSections; +}; diff --git a/apps/marketing/src/docs/referenceDocs.ts b/apps/marketing/src/docs/codeReferenceService.ts similarity index 75% rename from apps/marketing/src/docs/referenceDocs.ts rename to apps/marketing/src/docs/codeReferenceService.ts index e7fe51aa..d74d464a 100644 --- a/apps/marketing/src/docs/referenceDocs.ts +++ b/apps/marketing/src/docs/codeReferenceService.ts @@ -8,6 +8,9 @@ import { FileMetaDto, FileStructureDto, } from "@use-miller/shared-api-client"; +import { GetServerSidePropsContext } from "next"; +import { getAccessToken, getSession } from "@auth0/nextjs-auth0"; +import { createMenu } from "./leftMenuGeneration.js"; export type CodeExplorerData = { slug: string; @@ -120,7 +123,41 @@ export async function getCodeExplorerData( selectedFile: codeFile, }; } +export async function getCodeFileServerSideProps( + context: GetServerSidePropsContext +) { + const projectKey = context.params?.projectKey as string, + codeFile = context.params?.codeFile as string; + const session = await getSession(context.req, context.res); + let accessToken = null; + if (session) { + console.log("session", { session }); + const atResponse = await getAccessToken(context.req, context.res, { + scopes: ["openid", "email", "profile", "offline_access"], + }); + accessToken = atResponse.accessToken; + console.log("access token", { accessToken }); + } + if (!projectKey || !codeFile) { + throw new Error( + "Missing projectKey or codeFile - params strike again!" + ); + } + const codeExplorerData = await getCodeExplorerData( + projectKey, + codeFile, + accessToken + ); + const menuSections = await createMenu(); + + return { + props: { + menuSections, + codeExplorerData, + }, // will be passed to the page component as props + }; +} export async function getStaticReferenceDocsPageSlugs(): Promise<{ paths: { params: { diff --git a/apps/marketing/src/docs/docParser.ts b/apps/marketing/src/docs/docParser.ts index c589295c..7b988c03 100644 --- a/apps/marketing/src/docs/docParser.ts +++ b/apps/marketing/src/docs/docParser.ts @@ -12,10 +12,18 @@ import remarkRehype from "remark-rehype"; import remarkEmbedImages from "remark-embed-images"; import rehypeFormat from "rehype-format"; import rehypeStringify from "rehype-stringify"; -import { getAllCourses } from "@use-miller/shared-frontend-tooling"; -import { GetStaticPaths } from "next"; -const fileDirectory = path.join(process.cwd(), "src", "docs", "page-content"); +const docContentDirectory = path.join( + process.cwd(), + "src", + "docs", + "page-content" +); +export type Section = { + sectionDisplayName: string; + sectionSlug: string; + pages: SummaryDoc[]; +}; export type SummaryDoc = PostMatter & { slug: string; }; @@ -28,47 +36,103 @@ export type PostMatter = { export type FullDoc = SummaryDoc & { html: string; }; -export const defaultArticleSlug = "get-started-installation"; -export function getSortedPostsData(): SummaryDoc[] { - // Get file names under /posts - const fileNames = fs.readdirSync(fileDirectory, { withFileTypes: true }); - const allPostsData = fileNames - .filter((dirEntry) => dirEntry.isFile()) - .map((fileName) => { - // Remove ".md" from file name to get slug - const slug = fileName.name.replace(/\.md$/, ""); - // Read markdown file as string - const fullPath = path.join(fileDirectory, fileName.name); +export function toCapitalCase(str: string): string { + return str + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} +export function sortByOrder(a: SummaryDoc, b: SummaryDoc): number { + return a.order - b.order; +} +export function getSortedPostsData(): Section[] { + // Get file names for the sections + // expectation here is a structure like + // /my-section + // /my-page.md + // /my-other-page.md + // /folders-are-ignored-here + // /my-other-section + // ... + const sectionLevelDirItems = fs.readdirSync(docContentDirectory, { + withFileTypes: true, + }); + // get a list of the directories at the top level + const sectionDirectories = sectionLevelDirItems.filter((dirEntry) => + dirEntry.isDirectory() + ); + const allPostSections = sectionDirectories.map((sectionDirectory) => { + // section path + const sectionPath = path.join( + docContentDirectory, + sectionDirectory.name + ); + const sectionFiles = fs + .readdirSync(sectionPath, { + withFileTypes: true, + }) + .filter((dirEntry) => dirEntry.isFile()); + return { + sectionDisplayName: toCapitalCase( + sectionDirectory.name.replace(/-/g, " ") + ), + sectionSlug: sectionDirectory.name, + pages: sectionFiles + .map((fileName) => { + // Remove ".md" from file name to get slug + const slug = fileName.name.replace(/\.md$/, ""); - const fileContents = fs.readFileSync(fullPath, "utf8"); + // Read markdown file as string + const contentDocumentPath = path.join( + sectionPath, + fileName.name + ); - // Use gray-matter to parse the post metadata section - const matterResult = matter(fileContents); + const fileContents = fs.readFileSync( + contentDocumentPath, + "utf8" + ); - return { - slug, - ...matterResult.data, - }; - }) as unknown as SummaryDoc[]; - // Sort posts by date + // Use gray-matter to parse the post metadata section + const matterResult = matter(fileContents); - return allPostsData.sort((a, b) => { - if (a.section < b.section) { - return 1; - } else { - return -1; - } + return { + slug, + ...matterResult.data, + } as SummaryDoc; + }) + .sort(sortByOrder), + }; }); + console.log(allPostSections); + return allPostSections; } -export async function getSinglePost(slug?: string): Promise { - let renderSlug = slug; +export async function getSinglePost({ + slug, + sectionSlug, +}: { + slug?: string; + sectionSlug?: string; +}): Promise { + // if people are messing about with urls, just send them to the getting started page + // can improve this later + if ( + !sectionSlug || + sectionSlug === "index" || + sectionSlug === "" || + sectionSlug === "/" + ) { + sectionSlug = "get-started"; + slug = "quick-start"; + } if (!slug || slug === "index" || slug === "" || slug === "/") { - renderSlug = defaultArticleSlug; + sectionSlug = "get-started"; + slug = "quick-start"; } - const fullPath = path.join(fileDirectory, `${renderSlug}.md`); + const fullPath = path.join(docContentDirectory, sectionSlug, `${slug}.md`); const fileContents = fs.readFileSync(fullPath, "utf8"); // Use gray-matter to parse the post metadata section @@ -80,7 +144,7 @@ export async function getSinglePost(slug?: string): Promise { const markdownResult = await markdownToHtml(matterResult.content); // Combine the data with the id return { - slug: renderSlug!, + slug: slug!, html: markdownResult, ...matterResult.data, }; @@ -104,21 +168,28 @@ export async function getStaticDocsPageSlugs(): Promise<{ paths: { params: { slug: string; + section: string; }; }[]; fallback: boolean; }> { - const allPosts = getSortedPostsData(); + const allPostSections = getSortedPostsData(); - const paths = [ - ...allPosts.map((post) => { - return { + const paths = []; + for (const section of allPostSections) { + // skip reference section + if (section.sectionSlug === "reference") { + continue; + } + for (const page of section.pages) { + paths.push({ params: { - slug: post.slug, + section: section.sectionSlug, + slug: page.slug, }, - }; - }), - ]; + }); + } + } return { paths, diff --git a/apps/marketing/src/docs/leftMenu.ts b/apps/marketing/src/docs/leftMenu.ts deleted file mode 100644 index 833f1f1b..00000000 --- a/apps/marketing/src/docs/leftMenu.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - getAllCourses, - MenuItem, - MenuSection, -} from "@use-miller/shared-frontend-tooling"; -import { getSortedPostsData } from "./docParser.js"; - -export async function createMenu(): Promise { - if (fetch === undefined) { - throw new Error("fetch is undefined"); - } - const projects = await getAllCourses({ - apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH, - fetchApi: fetch, - }); - const allArticles = getSortedPostsData(); - - const menuSections: MenuSection[] = [ - { - // get started first! - name: "Get Started", - items: - allArticles - .filter((p) => p.section === "Get Started") - .sort((a, b) => a.order - b.order) - .map((post) => { - return { - name: post.title, - path: `/docs/${post.slug}`, - } as MenuItem; - }) || [], - }, - { - // then the project references - name: "Projects", - items: - projects.map((project) => { - return { - name: project.name, - path: `/docs/reference/${project.key}/${btoa( - "/README.md" - )}`, - } as MenuItem; - }) || [], - }, - ]; - - return menuSections; -} diff --git a/apps/marketing/src/docs/leftMenuGeneration.ts b/apps/marketing/src/docs/leftMenuGeneration.ts new file mode 100644 index 00000000..2b6454a1 --- /dev/null +++ b/apps/marketing/src/docs/leftMenuGeneration.ts @@ -0,0 +1,60 @@ +import { + getAllCourses, + MenuItem, + MenuSection, +} from "@use-miller/shared-frontend-tooling"; +import { getSortedPostsData } from "./docParser.js"; + +export async function sortByCustomSlugMapping( + menuSections: MenuSection[] +): Promise { + const customOrderSections: MenuSection[] = [ + menuSections.find((section) => section.slug === "get-started")!, + menuSections.find((section) => section.slug === "reference")!, + ]; + const otherSections = menuSections.filter( + (section) => !customOrderSections.find((s) => s.slug === section.slug) + ); + return [...customOrderSections, ...otherSections]; +} + +export async function createMenu(): Promise { + if (fetch === undefined) { + throw new Error( + "fetch is undefined. This should never happen but most likely you're using a very old browser or old nodeJS." + ); + } + const projects = await getAllCourses({ + apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH, + fetchApi: fetch, + }); + // add all the docs that were found + const allMenuItems = getSortedPostsData().map((section) => { + return { + name: section.sectionDisplayName, + slug: section.sectionSlug, + items: section.pages.map((page) => { + return { + name: page.title, + path: `/docs/${section.sectionSlug}/${page.slug}`, + } as MenuItem; + }), + } as MenuSection; + }); + // then the code project references + allMenuItems.push({ + name: "Code Reference", + slug: "reference", + items: + projects.map((project) => { + return { + name: project.name, + path: `/docs/reference/${project.key}/${btoa( + "/README.md" + )}`, + } as MenuItem; + }) || [], + }); + console.log(allMenuItems); + return sortByCustomSlugMapping(allMenuItems); +} diff --git a/apps/marketing/src/docs/page-content/get-started-basic-features.md b/apps/marketing/src/docs/page-content/get-started/basic-features.md similarity index 98% rename from apps/marketing/src/docs/page-content/get-started-basic-features.md rename to apps/marketing/src/docs/page-content/get-started/basic-features.md index 264f8e2f..b58e64f8 100644 --- a/apps/marketing/src/docs/page-content/get-started-basic-features.md +++ b/apps/marketing/src/docs/page-content/get-started/basic-features.md @@ -1,8 +1,7 @@ --- title: "Basic Features" date: "2020-01-01" -section: "Get Started" -order: 2 +order: 3 --- Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. diff --git a/apps/marketing/src/docs/page-content/diagrams/miller.drawio.svg b/apps/marketing/src/docs/page-content/get-started/diagrams/miller.drawio.svg similarity index 100% rename from apps/marketing/src/docs/page-content/diagrams/miller.drawio.svg rename to apps/marketing/src/docs/page-content/get-started/diagrams/miller.drawio.svg diff --git a/apps/marketing/src/docs/page-content/get-started-installation.md b/apps/marketing/src/docs/page-content/get-started/installation.md similarity index 98% rename from apps/marketing/src/docs/page-content/get-started-installation.md rename to apps/marketing/src/docs/page-content/get-started/installation.md index d5e73740..ea208db7 100644 --- a/apps/marketing/src/docs/page-content/get-started-installation.md +++ b/apps/marketing/src/docs/page-content/get-started/installation.md @@ -1,8 +1,7 @@ --- title: "Installation" date: "2020-01-01" -section: "Get Started" -order: 1 +order: 2 --- Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. diff --git a/apps/marketing/src/docs/page-content/get-started/quick-start.md b/apps/marketing/src/docs/page-content/get-started/quick-start.md new file mode 100644 index 00000000..33b3c261 --- /dev/null +++ b/apps/marketing/src/docs/page-content/get-started/quick-start.md @@ -0,0 +1,26 @@ +--- +title: "Quick Start" +date: "2020-01-01" +order: 1 +--- + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. diff --git a/apps/marketing/src/docs/page-content/how-to/basic-features.md b/apps/marketing/src/docs/page-content/how-to/basic-features.md new file mode 100644 index 00000000..b58e64f8 --- /dev/null +++ b/apps/marketing/src/docs/page-content/how-to/basic-features.md @@ -0,0 +1,26 @@ +--- +title: "Basic Features" +date: "2020-01-01" +order: 3 +--- + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. diff --git a/apps/marketing/src/docs/page-content/how-to/diagrams/miller.drawio.svg b/apps/marketing/src/docs/page-content/how-to/diagrams/miller.drawio.svg new file mode 100644 index 00000000..388f5e01 --- /dev/null +++ b/apps/marketing/src/docs/page-content/how-to/diagrams/miller.drawio.svg @@ -0,0 +1,108 @@ + + + + + + + + + +
    +
    +
    + Main app +
    +
    +
    +
    + + Main app + +
    +
    + + + + + + +
    +
    +
    + Prebuilt integrations +
    +
    +
    +
    + + Prebuilt integrations + +
    +
    + + + + + + +
    +
    +
    + Backend APi +
    +
    +
    +
    + + Backend APi + +
    +
    + + + + + + +
    +
    +
    + Marketing Site +
    +
    +
    +
    + + Marketing Site + +
    +
    + + + + + + +
    +
    +
    + Auth0 +
    +
    +
    +
    + + Auth0 + +
    +
    +
    + + + + + Text is not SVG - cannot display + + + +
    \ No newline at end of file diff --git a/apps/marketing/src/docs/page-content/how-to/installation.md b/apps/marketing/src/docs/page-content/how-to/installation.md new file mode 100644 index 00000000..ea208db7 --- /dev/null +++ b/apps/marketing/src/docs/page-content/how-to/installation.md @@ -0,0 +1,26 @@ +--- +title: "Installation" +date: "2020-01-01" +order: 2 +--- + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. diff --git a/apps/marketing/src/docs/page-content/how-to/quick-start.md b/apps/marketing/src/docs/page-content/how-to/quick-start.md new file mode 100644 index 00000000..33b3c261 --- /dev/null +++ b/apps/marketing/src/docs/page-content/how-to/quick-start.md @@ -0,0 +1,26 @@ +--- +title: "Quick Start" +date: "2020-01-01" +order: 1 +--- + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. + +Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. + +- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. +- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. + +Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. diff --git a/apps/marketing/src/home-cta/Hero.tsx b/apps/marketing/src/home-cta/Hero.tsx index edd1995d..2c26a20d 100644 --- a/apps/marketing/src/home-cta/Hero.tsx +++ b/apps/marketing/src/home-cta/Hero.tsx @@ -52,16 +52,19 @@ export function Hero() {
    admin image product image code image { + const [formattedDate, setFormattedDate] = useState("{local date}"); + + useEffect(() => setFormattedDate(new Date(date).toLocaleDateString()), []); + + return formattedDate; +}; diff --git a/apps/marketing/src/hooks/useGetCustomerPortalSession.ts b/apps/marketing/src/hooks/useGetCustomerPortalSession.ts new file mode 100644 index 00000000..84b10295 --- /dev/null +++ b/apps/marketing/src/hooks/useGetCustomerPortalSession.ts @@ -0,0 +1,54 @@ +import { + PaymentsApi, + StripeCustomerPortalRequestDto, + StripeCustomerPortalResponseDto, +} from "@use-miller/shared-api-client"; +import { getAuthenticatedApiInstance } from "@use-miller/shared-frontend-tooling"; +import { useMutation } from "@tanstack/react-query"; +import { getAccessToken } from "@auth0/nextjs-auth0"; +import { NextApiRequest, NextApiResponse } from "next"; + +// there is a bit of misdirection here so we can use the +// access token. We call the next api which in turn calls the +// backend api 🤷‍♂️ +export async function getStripeCustomerPortalLink( + req: NextApiRequest, + res: NextApiResponse +) { + const requestBody = req.body as StripeCustomerPortalRequestDto; + const atResponse = await getAccessToken(req, res, { + scopes: ["openid", "email", "profile", "offline_access"], + }); + const apiClient = await getAuthenticatedApiInstance( + PaymentsApi, + process.env.NEXT_PUBLIC_API_BASE_PATH!, + atResponse.accessToken!, + fetch + ); + + return await apiClient.stripeCustomerPortalControllerCreateCustomerPortalSession( + { + stripeCustomerPortalRequestDto: requestBody, + } + ); +} + +export function useGetCustomerPortalSession() { + return useMutation( + ["getCustomerPortalSession"], + async ( + variables: StripeCustomerPortalRequestDto + ): Promise => { + const response = await fetch("/api/stripe/customer-portal-link", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(variables), + }); + const result = + (await response.json()) as StripeCustomerPortalResponseDto; + return result; + } + ); +} diff --git a/apps/marketing/src/hooks/useGetPaymentLink.ts b/apps/marketing/src/hooks/useGetPaymentLink.ts new file mode 100644 index 00000000..11a4de9e --- /dev/null +++ b/apps/marketing/src/hooks/useGetPaymentLink.ts @@ -0,0 +1,49 @@ +import { + PaymentsApi, + StripeCheckoutSessionRequestDto, + StripeCheckoutSessionResponseDto, +} from "@use-miller/shared-api-client"; +import { getAuthenticatedApiInstance } from "@use-miller/shared-frontend-tooling"; +import { useMutation } from "@tanstack/react-query"; +import { NextApiRequest, NextApiResponse } from "next"; +import { getAccessToken } from "@auth0/nextjs-auth0"; + +export async function getStripeCheckoutLink( + req: NextApiRequest, + res: NextApiResponse +) { + const requestBody = req.body as StripeCheckoutSessionRequestDto; + const atResponse = await getAccessToken(req, res, { + scopes: ["openid", "email", "profile", "offline_access"], + }); + const apiClient = await getAuthenticatedApiInstance( + PaymentsApi, + process.env.NEXT_PUBLIC_API_BASE_PATH!, + atResponse.accessToken!, + fetch + ); + + return await apiClient.stripeCheckoutControllerCreateCheckoutSession({ + stripeCheckoutSessionRequestDto: requestBody, + }); +} + +export function useGetPaymentLink() { + return useMutation( + ["getCheckoutSession"], + async ( + variables: StripeCheckoutSessionRequestDto + ): Promise => { + const response = await fetch("/api/stripe/checkout-link", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(variables), + }); + const result = + (await response.json()) as StripeCheckoutSessionResponseDto; + return result; + } + ); +} diff --git a/apps/marketing/src/pages/api/stripe/checkout-link.ts b/apps/marketing/src/pages/api/stripe/checkout-link.ts new file mode 100644 index 00000000..3236dab1 --- /dev/null +++ b/apps/marketing/src/pages/api/stripe/checkout-link.ts @@ -0,0 +1,4 @@ +import { withApiAuthRequired } from "@auth0/nextjs-auth0"; +import { getStripeCheckoutLink } from "../../../hooks/useGetPaymentLink.js"; + +export default withApiAuthRequired(getStripeCheckoutLink); diff --git a/apps/marketing/src/pages/api/stripe/customer-portal-link.ts b/apps/marketing/src/pages/api/stripe/customer-portal-link.ts new file mode 100644 index 00000000..f9029bf0 --- /dev/null +++ b/apps/marketing/src/pages/api/stripe/customer-portal-link.ts @@ -0,0 +1,4 @@ +import { withApiAuthRequired } from "@auth0/nextjs-auth0"; +import { getStripeCustomerPortalLink } from "../../../hooks/useGetCustomerPortalSession.js"; + +export default withApiAuthRequired(getStripeCustomerPortalLink); diff --git a/apps/marketing/src/pages/dashboard/[orgUuid].tsx b/apps/marketing/src/pages/dashboard/[orgUuid].tsx new file mode 100644 index 00000000..35ab7ff0 --- /dev/null +++ b/apps/marketing/src/pages/dashboard/[orgUuid].tsx @@ -0,0 +1,34 @@ +import { MenuSection } from "../../components/LeftMenu.jsx"; +import { withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { dashboardGetSspData } from "../../dashboard/dashboardDataService.js"; +import { + Organisation, + OrganisationSubscriptionRecord, +} from "@use-miller/shared-api-client"; +import { DashboardDetails } from "../../dashboard/components/DashboardDetails.jsx"; +import { LeftMenuWrappedContent } from "../../components/LeftMenuWrappedContent.jsx"; + +export const getServerSideProps = withPageAuthRequired({ + // returnTo: '/unauthorized', + getServerSideProps: dashboardGetSspData, +}); + +export default function Home({ + menuSections, + currentOrg, + subs, +}: { + menuSections: MenuSection[]; + currentOrg: Organisation; + subs: OrganisationSubscriptionRecord[]; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/dashboard/account/profile.tsx b/apps/marketing/src/pages/dashboard/account/profile.tsx new file mode 100644 index 00000000..74572ee4 --- /dev/null +++ b/apps/marketing/src/pages/dashboard/account/profile.tsx @@ -0,0 +1,46 @@ +import { getAccessToken, withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { User } from "@use-miller/shared-api-client"; +import { MenuSection } from "@use-miller/shared-frontend-tooling"; +import { GetServerSidePropsContext, PreviewData } from "next"; +import { ParsedUrlQuery } from "querystring"; +import { LeftMenuWrappedContent } from "../../../components/LeftMenuWrappedContent.jsx"; +import { ProfileDetails } from "../../../dashboard/components/ProfileDetails.jsx"; +import { getAccountIndexData } from "../../../dashboard/dashboardDataService.js"; + +export const getServerSideProps = withPageAuthRequired({ + // returnTo: '/unauthorized', + getServerSideProps: customGetSSP, +}); + +export async function customGetSSP( + context: GetServerSidePropsContext +) { + const atResponse = await getAccessToken(context.req, context.res, { + scopes: ["openid", "email", "profile", "offline_access"], + }); + + const data = await getAccountIndexData( + atResponse.accessToken! // user can't be logged in + ); + return { + props: data, + }; +} + +export default function Home({ + menuSections, + currentUser, +}: { + menuSections: MenuSection[]; + currentUser: User; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/dashboard/index.tsx b/apps/marketing/src/pages/dashboard/index.tsx new file mode 100644 index 00000000..35ab7ff0 --- /dev/null +++ b/apps/marketing/src/pages/dashboard/index.tsx @@ -0,0 +1,34 @@ +import { MenuSection } from "../../components/LeftMenu.jsx"; +import { withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { dashboardGetSspData } from "../../dashboard/dashboardDataService.js"; +import { + Organisation, + OrganisationSubscriptionRecord, +} from "@use-miller/shared-api-client"; +import { DashboardDetails } from "../../dashboard/components/DashboardDetails.jsx"; +import { LeftMenuWrappedContent } from "../../components/LeftMenuWrappedContent.jsx"; + +export const getServerSideProps = withPageAuthRequired({ + // returnTo: '/unauthorized', + getServerSideProps: dashboardGetSspData, +}); + +export default function Home({ + menuSections, + currentOrg, + subs, +}: { + menuSections: MenuSection[]; + currentOrg: Organisation; + subs: OrganisationSubscriptionRecord[]; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/docs/[section]/[slug].tsx b/apps/marketing/src/pages/docs/[section]/[slug].tsx new file mode 100644 index 00000000..60505f5c --- /dev/null +++ b/apps/marketing/src/pages/docs/[section]/[slug].tsx @@ -0,0 +1,51 @@ +import { + FullDoc, + getStaticDocsPageSlugs, + getSinglePost, +} from "../../../docs/docParser.js"; + +import { GetStaticPaths } from "next"; +import { createMenu } from "../../../docs/leftMenuGeneration.js"; +import { DocArticle } from "../../../docs/components/DocArticle.jsx"; +import { MenuSection } from "../../../components/LeftMenu.jsx"; +import { LeftMenuWrappedContent } from "../../../components/LeftMenuWrappedContent.jsx"; + +export async function getStaticProps({ + params, +}: { + params: { slug?: string; section?: string }; +}) { + const article = await getSinglePost({ + slug: params.slug, + sectionSlug: params.section, + }); + const menuSections = await createMenu(); + return { + props: { + menuSections, + article, + }, + }; +} + +export const getStaticPaths: GetStaticPaths = async () => { + return getStaticDocsPageSlugs(); +}; + +export default function Home({ + menuSections, + article, +}: { + menuSections: MenuSection[]; + article: FullDoc; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/docs/[section]/index.tsx b/apps/marketing/src/pages/docs/[section]/index.tsx new file mode 100644 index 00000000..0848c89e --- /dev/null +++ b/apps/marketing/src/pages/docs/[section]/index.tsx @@ -0,0 +1,49 @@ +import { + FullDoc, + getSinglePost, + getStaticDocsPageSlugs, +} from "../../../docs/docParser.js"; +import { createMenu } from "../../../docs/leftMenuGeneration.js"; +import { DocArticle } from "../../../docs/components/DocArticle.jsx"; +import { MenuSection } from "../../../components/LeftMenu.jsx"; +import { LeftMenuWrappedContent } from "../../../components/LeftMenuWrappedContent.jsx"; +import { GetStaticPaths } from "next"; + +export async function getStaticProps({ + params, +}: { + params: { section?: string }; +}) { + const article = await getSinglePost({ + slug: "/", + sectionSlug: params.section, + }); + const menuSections = await createMenu(); + return { + props: { + menuSections, + article, + }, + }; +} +export const getStaticPaths: GetStaticPaths = async () => { + return getStaticDocsPageSlugs(); +}; + +export default function Home({ + menuSections, + article, +}: { + menuSections: MenuSection[]; + article: FullDoc; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/docs/[slug].tsx b/apps/marketing/src/pages/docs/[slug].tsx deleted file mode 100644 index c25e8fb2..00000000 --- a/apps/marketing/src/pages/docs/[slug].tsx +++ /dev/null @@ -1,49 +0,0 @@ -import Layout from "../../components/Layout.jsx"; -import { - FullDoc, - getStaticDocsPageSlugs, - getSinglePost, -} from "../../docs/docParser.js"; -import { Container } from "../../components/Container.jsx"; -import { GetStaticPaths } from "next"; -import { createMenu } from "../../docs/leftMenu.js"; -import { DocArticle } from "../../docs/components/DocArticle.jsx"; -import { LeftMenu, MenuSection } from "../../docs/components/LeftMenu.jsx"; - -export async function getStaticProps({ - params, -}: { - params: { slug?: string }; -}) { - const article = await getSinglePost(params.slug); - const menuSections = await createMenu(); - return { - props: { - menuSections, - article, - }, - }; -} - -export const getStaticPaths: GetStaticPaths = async () => { - return getStaticDocsPageSlugs(); -}; - -export default function Home({ - menuSections, - article, -}: { - menuSections: MenuSection[]; - article: FullDoc; -}) { - return ( - - -
    - - -
    -
    -
    - ); -} diff --git a/apps/marketing/src/pages/docs/index.tsx b/apps/marketing/src/pages/docs/index.tsx index 9c7899d8..9bb507f3 100644 --- a/apps/marketing/src/pages/docs/index.tsx +++ b/apps/marketing/src/pages/docs/index.tsx @@ -1,12 +1,18 @@ -import Layout from "../../components/Layout.js"; import { FullDoc, getSinglePost } from "../../docs/docParser.js"; -import { Container } from "../../components/Container.js"; -import { createMenu } from "../../docs/leftMenu.js"; +import { createMenu } from "../../docs/leftMenuGeneration.js"; import { DocArticle } from "../../docs/components/DocArticle.jsx"; -import { LeftMenu, MenuSection } from "../../docs/components/LeftMenu.jsx"; +import { MenuSection } from "../../components/LeftMenu.jsx"; +import { LeftMenuWrappedContent } from "../../components/LeftMenuWrappedContent.jsx"; -export async function getStaticProps() { - const article = await getSinglePost("/"); +export async function getStaticProps({ + params, +}: { + params: { section?: string }; +}) { + const article = await getSinglePost({ + slug: "", + sectionSlug: "", + }); const menuSections = await createMenu(); return { props: { @@ -24,13 +30,12 @@ export default function Home({ article: FullDoc; }) { return ( - - -
    - - -
    -
    -
    + + + ); } diff --git a/apps/marketing/src/pages/docs/reference/[projectKey]/[codeFile].tsx b/apps/marketing/src/pages/docs/reference/[projectKey]/[codeFile].tsx index b5b2bfd4..8a16972f 100644 --- a/apps/marketing/src/pages/docs/reference/[projectKey]/[codeFile].tsx +++ b/apps/marketing/src/pages/docs/reference/[projectKey]/[codeFile].tsx @@ -1,52 +1,16 @@ -import Layout from "../../../../components/Layout.jsx"; -import { Container } from "../../../../components/Container.jsx"; import { GetServerSidePropsContext } from "next"; -import { createMenu } from "../../../../docs/leftMenu.js"; -import { - LeftMenu, - MenuSection, -} from "../../../../docs/components/LeftMenu.jsx"; +import { MenuSection } from "../../../../components/LeftMenu.jsx"; import { CodeExplorerData, - getCodeExplorerData, -} from "../../../../docs/referenceDocs.js"; + getCodeFileServerSideProps, +} from "../../../../docs/codeReferenceService.js"; import dynamic from "next/dynamic"; import { useRouter } from "next/router.js"; -import { getAccessToken, getSession } from "@auth0/nextjs-auth0"; +import { LeftMenuWrappedContent } from "../../../../components/LeftMenuWrappedContent.jsx"; export async function getServerSideProps(context: GetServerSidePropsContext) { - const projectKey = context.params?.projectKey as string, - codeFile = context.params?.codeFile as string; - const session = await getSession(context.req, context.res); - let accessToken = null; - if (session) { - console.log("session", { session }); - const atResponse = await getAccessToken(context.req, context.res, { - scopes: ["openid", "email", "profile", "offline_access"], - }); - accessToken = atResponse.accessToken; - console.log("access token", { accessToken }); - } - if (!projectKey || !codeFile) { - throw new Error( - "Missing projectKey or codeFile - params strike again!" - ); - } - - const initialData = await getCodeExplorerData( - projectKey, - codeFile, - accessToken - ); - const menuSections = await createMenu(); - - return { - props: { - menuSections, - codeExplorerData: initialData, - }, // will be passed to the page component as props - }; + return getCodeFileServerSideProps(context); } const DynamicCodeExplorer = dynamic( @@ -81,7 +45,7 @@ export default function CodeFileHome({ slug: projectKeyX, } = codeExplorerData; - let codeComp = ( + let codeComponent = ( Loading...

    ; + codeComponent =

    Loading...

    ; } return ( - - -
    - -
    - {codeComp} -
    -
    -
    -
    + +
    + {codeComponent} +
    +
    ); } diff --git a/apps/marketing/src/pages/super-admin/org-subs.tsx b/apps/marketing/src/pages/super-admin/org-subs.tsx new file mode 100644 index 00000000..876bac8f --- /dev/null +++ b/apps/marketing/src/pages/super-admin/org-subs.tsx @@ -0,0 +1,29 @@ +import { MenuSection } from "../../components/LeftMenu.jsx"; +import { withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { OrganisationSubscriptionRecord } from "@use-miller/shared-api-client"; +import { LeftMenuWrappedContent } from "../../components/LeftMenuWrappedContent.jsx"; +import { superUserGetSubscriptionsData } from "../../super-admin/services/superAdminData.js"; +import OrgSubsSuperAdmin from "../../super-admin/components/OrgSubsSuperAdmin.jsx"; + +export const getServerSideProps = withPageAuthRequired({ + // returnTo: '/unauthorized', + getServerSideProps: superUserGetSubscriptionsData, +}); + +export default function Home({ + menuSections, + allSubs, +}: { + menuSections: MenuSection[]; + allSubs: OrganisationSubscriptionRecord[]; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/super-admin/payment-events.tsx b/apps/marketing/src/pages/super-admin/payment-events.tsx new file mode 100644 index 00000000..213e76cd --- /dev/null +++ b/apps/marketing/src/pages/super-admin/payment-events.tsx @@ -0,0 +1,29 @@ +import { MenuSection } from "../../components/LeftMenu.jsx"; +import { withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { StripeCheckoutEvent } from "@use-miller/shared-api-client"; +import { LeftMenuWrappedContent } from "../../components/LeftMenuWrappedContent.jsx"; +import { superUserGetPaymentData } from "../../super-admin/services/superAdminData.js"; +import PaymentEventsSuperAdmin from "../../super-admin/components/PaymentEventsSuperAdmin.jsx"; + +export const getServerSideProps = withPageAuthRequired({ + // returnTo: '/unauthorized', + getServerSideProps: superUserGetPaymentData, +}); + +export default function Home({ + menuSections, + allData, +}: { + menuSections: MenuSection[]; + allData: StripeCheckoutEvent[]; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/pages/super-admin/users.tsx b/apps/marketing/src/pages/super-admin/users.tsx new file mode 100644 index 00000000..c0b1e24e --- /dev/null +++ b/apps/marketing/src/pages/super-admin/users.tsx @@ -0,0 +1,29 @@ +import { MenuSection } from "../../components/LeftMenu.jsx"; +import { withPageAuthRequired } from "@auth0/nextjs-auth0"; +import { User } from "@use-miller/shared-api-client"; +import { LeftMenuWrappedContent } from "../../components/LeftMenuWrappedContent.jsx"; +import UsersSuperAdmin from "../../super-admin/components/UsersSuperAdmin.jsx"; +import { superUserGetUserData } from "../../super-admin/services/superAdminData.js"; + +export const getServerSideProps = withPageAuthRequired({ + // returnTo: '/unauthorized', + getServerSideProps: superUserGetUserData, +}); + +export default function Home({ + menuSections, + allUsers, +}: { + menuSections: MenuSection[]; + allUsers: User[]; +}) { + return ( + + + + ); +} diff --git a/apps/marketing/src/super-admin/components/OrgSubsSuperAdmin.tsx b/apps/marketing/src/super-admin/components/OrgSubsSuperAdmin.tsx new file mode 100644 index 00000000..f2fe16ee --- /dev/null +++ b/apps/marketing/src/super-admin/components/OrgSubsSuperAdmin.tsx @@ -0,0 +1,192 @@ +import { OrganisationSubscriptionRecord } from "@use-miller/shared-api-client"; +import React from "react"; +import { useFormattedDate } from "../../hooks/useFormattedDate.js"; + +const ProductLink = ({ sp }: { sp: OrganisationSubscriptionRecord }) => { + if (sp.paymentSystemName === "stripe") { + return ( + + {sp.paymentSystemProductId} + + ); + } + return <>{sp.paymentSystemProductId}; +}; + +const TransactionLink = ({ sp }: { sp: OrganisationSubscriptionRecord }) => { + if (sp.paymentSystemName === "stripe") { + if (sp.paymentSystemTransactionId.startsWith("sub_")) { + return ( + + {sp.paymentSystemTransactionId} + + ); + } + if (sp.paymentSystemTransactionId.startsWith("pi_")) { + return ( + + {sp.paymentSystemProductId} + + ); + } + } + return <>{sp.paymentSystemProductId}; +}; + +const CustomerLink = ({ sp }: { sp: OrganisationSubscriptionRecord }) => { + if (sp.paymentSystemName === "stripe") { + return ( + + {sp.paymentSystemCustomerId} + + ); + } + return <>{sp.paymentSystemCustomerId}; +}; + +const OrgSubsSuperAdmin = ({ + allSubs, + title, +}: { + allSubs: OrganisationSubscriptionRecord[]; + title: string; +}) => { + return ( +
    +

    {title}

    +
    +
    +
    + + + + + + + + + + + + + + + + + {allSubs && + allSubs.map((sp) => { + const createdDate = useFormattedDate( + sp.createdDate + ); + const validUntil = useFormattedDate( + sp.validUntil + ); + return ( + + + + + + + + + + + + ); + })} + +
    + Created + + Valid Until + + Product Name + + Email + + Customer Id + + Type + + Provider + + Product Id + + Transaction Id +
    + {createdDate} + + {validUntil} + + {sp.productDisplayName} + + { + sp.paymentSystemCustomerEmail + } + + + + {sp.paymentSystemMode} + + {sp.paymentSystemName} + + + + +
    +
    +
    +
    +
    + ); +}; + +export default OrgSubsSuperAdmin; diff --git a/apps/marketing/src/super-admin/components/PaymentEventsSuperAdmin.tsx b/apps/marketing/src/super-admin/components/PaymentEventsSuperAdmin.tsx new file mode 100644 index 00000000..628f4d91 --- /dev/null +++ b/apps/marketing/src/super-admin/components/PaymentEventsSuperAdmin.tsx @@ -0,0 +1,75 @@ +import { StripeCheckoutEvent } from "@use-miller/shared-api-client"; +import React from "react"; +import { useFormattedDate } from "../../hooks/useFormattedDate.js"; + +const PaymentEventsSuperAdmin = ({ + allData, + title, +}: { + allData: StripeCheckoutEvent[]; + title: string; +}) => { + return ( +
    +

    {title}

    +

    + Payment events are the webhooks that have been processed by your + app. You can use these for quick debugging and to see what is + happening. For more details log into your database or check the + payment provider's dashboard. +

    +
    +
    + {allData && + allData.map((sp) => { + const createdDate = useFormattedDate( + sp.createdDate + ); + return ( +
    +
    + <> +
    +
    + Created At +
    +
    + {createdDate} +
    +
    +
    +
    + Client Reference +
    +
    + {sp.clientReferenceId} +
    +
    + +
    +
    + Event Data +
    +
    + {" "} +
    +                                                        
    +                                                            {
    +                                                                sp.stripeDataAsString
    +                                                            }
    +                                                        
    +                                                    
    +
    +
    + +
    +
    + ); + })} +
    +
    +
    + ); +}; + +export default PaymentEventsSuperAdmin; diff --git a/apps/marketing/src/super-admin/components/UsersSuperAdmin.tsx b/apps/marketing/src/super-admin/components/UsersSuperAdmin.tsx new file mode 100644 index 00000000..13a3a590 --- /dev/null +++ b/apps/marketing/src/super-admin/components/UsersSuperAdmin.tsx @@ -0,0 +1,110 @@ +import { User } from "@use-miller/shared-api-client"; +import React from "react"; +import { useFormattedDate } from "../../hooks/useFormattedDate.js"; + +const UsersSuperAdmin = ({ + allUsers, + title, +}: { + allUsers: User[]; + title: string; +}) => { + return ( +
    +

    {title}

    +
    +
    +
    + + + + + + + + + + + + + + + + {allUsers && + allUsers.map((user) => { + const createdDate = useFormattedDate( + user.createdDate + ); + return ( + + + + + + + + + + + + ); + })} + +
    + Id + + First name + + Surname + + Email + + Joined + + Uuid + + Auth0 Id +
    + {user.id} + + {user.givenName} + + {user.familyName} + + {user.email} + + {createdDate} + + {user.uuid} + + {user.auth0UserId} +
    +
    +
    +
    +
    + ); +}; + +export default UsersSuperAdmin; diff --git a/apps/marketing/src/super-admin/services/superAdminData.ts b/apps/marketing/src/super-admin/services/superAdminData.ts new file mode 100644 index 00000000..a2129937 --- /dev/null +++ b/apps/marketing/src/super-admin/services/superAdminData.ts @@ -0,0 +1,131 @@ +import { + OrganisationSubscriptionsApi, + PaymentsApi, + UsersApi, +} from "@use-miller/shared-api-client"; +import { getAuthenticatedApiInstance } from "@use-miller/shared-frontend-tooling"; +import { getAccessToken } from "@auth0/nextjs-auth0"; +import { GetServerSidePropsContext, PreviewData } from "next"; +import { ParsedUrlQuery } from "querystring"; + +const superUserScopes = [ + "openid", + "email", + "read:all", + "modify:all", + "profile", + "offline_access", +]; +export async function superUserGetUserData( + context: GetServerSidePropsContext +) { + const atResponse = await getAccessToken(context.req, context.res, { + scopes: superUserScopes, + }); + const apiClient = await getAuthenticatedApiInstance( + UsersApi, + process.env.NEXT_PUBLIC_API_BASE_PATH!, + atResponse.accessToken!, + fetch + ); + const [allUsers, menuSections] = await Promise.allSettled([ + apiClient.userControllerFindAll(), + createMenu(), + ]); + if (allUsers.status === "rejected" || menuSections.status === "rejected") { + throw new Error("Failed to get data"); + } + + return { + props: { + allUsers: allUsers.value, + menuSections: menuSections.value, + }, + }; +} + +export const createMenu = () => { + const menuSections = []; + + menuSections.push({ + name: "Super Powers", + slug: "super-powers", + items: [ + { + name: "Users", + path: "/super-admin/users", + }, + { + name: "Org Subs", + path: "/super-admin/org-subs", + }, + { + name: "Payment Events", + path: "/super-admin/payment-events", + }, + ], + }); + + return menuSections; +}; + +export async function superUserGetPaymentData( + context: GetServerSidePropsContext +) { + const atResponse = await getAccessToken(context.req, context.res, { + scopes: superUserScopes, + }); + const apiClient = await getAuthenticatedApiInstance( + PaymentsApi, + process.env.NEXT_PUBLIC_API_BASE_PATH!, + atResponse.accessToken!, + fetch + ); + + const [allData, menuSections] = await Promise.allSettled([ + apiClient.stripeEventsControllerGetLastEvents({ + skip: 0, + take: 20, + }), + createMenu(), + ]); + if (allData.status === "rejected" || menuSections.status === "rejected") { + throw new Error("Failed to get data"); + } + + return { + props: { + allData: allData.value, + menuSections: menuSections.value, + }, + }; +} + +export async function superUserGetSubscriptionsData( + context: GetServerSidePropsContext +) { + const atResponse = await getAccessToken(context.req, context.res, { + scopes: superUserScopes, + }); + const apiClient = await getAuthenticatedApiInstance( + OrganisationSubscriptionsApi, + process.env.NEXT_PUBLIC_API_BASE_PATH!, + atResponse.accessToken!, + fetch + ); + + const [orgSubs, menuSections] = await Promise.allSettled([ + apiClient.allSubscriptionsControllerFindAll(), + createMenu(), + ]); + if (orgSubs.status === "rejected" || menuSections.status === "rejected") { + throw new Error("Failed to get data"); + } + + return { + props: { + allSubs: orgSubs.value, + menuSections: menuSections.value, + }, + }; +} diff --git a/libs/shared-api-client/src/models/MembershipRole.ts b/libs/shared-api-client/src/models/MembershipRole.ts index d9c2229d..76dbfd95 100644 --- a/libs/shared-api-client/src/models/MembershipRole.ts +++ b/libs/shared-api-client/src/models/MembershipRole.ts @@ -49,12 +49,6 @@ export interface MembershipRole { * @memberof MembershipRole */ updateDate: Date; - /** - * - * @type {Date} - * @memberof MembershipRole - */ - deletedDate: Date; } export function MembershipRoleFromJSON(json: any): MembershipRole { @@ -72,7 +66,6 @@ export function MembershipRoleFromJSONTyped(json: any, ignoreDiscriminator: bool 'name': json['name'], 'createdDate': (new Date(json['createdDate'])), 'updateDate': (new Date(json['updateDate'])), - 'deletedDate': (new Date(json['deletedDate'])), }; } @@ -90,7 +83,6 @@ export function MembershipRoleToJSON(value?: MembershipRole | null): any { 'name': value.name, 'createdDate': (value.createdDate.toISOString()), 'updateDate': (value.updateDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), }; } diff --git a/libs/shared-api-client/src/models/Organisation.ts b/libs/shared-api-client/src/models/Organisation.ts index 4a8c09cf..f956a075 100644 --- a/libs/shared-api-client/src/models/Organisation.ts +++ b/libs/shared-api-client/src/models/Organisation.ts @@ -54,7 +54,7 @@ export interface Organisation { * @type {Date} * @memberof Organisation */ - deletedDate: Date; + deletedDate?: Date; } export function OrganisationFromJSON(json: any): Organisation { @@ -72,7 +72,7 @@ export function OrganisationFromJSONTyped(json: any, ignoreDiscriminator: boolea 'name': json['name'], 'createdDate': (new Date(json['createdDate'])), 'updateDate': (new Date(json['updateDate'])), - 'deletedDate': (new Date(json['deletedDate'])), + 'deletedDate': !exists(json, 'deletedDate') ? undefined : (new Date(json['deletedDate'])), }; } @@ -90,7 +90,7 @@ export function OrganisationToJSON(value?: Organisation | null): any { 'name': value.name, 'createdDate': (value.createdDate.toISOString()), 'updateDate': (value.updateDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), + 'deletedDate': value.deletedDate === undefined ? undefined : (value.deletedDate.toISOString()), }; } diff --git a/libs/shared-api-client/src/models/OrganisationMembership.ts b/libs/shared-api-client/src/models/OrganisationMembership.ts index 268530a8..5d68634c 100644 --- a/libs/shared-api-client/src/models/OrganisationMembership.ts +++ b/libs/shared-api-client/src/models/OrganisationMembership.ts @@ -55,7 +55,7 @@ export interface OrganisationMembership { * @type {Array} * @memberof OrganisationMembership */ - roles: Array; + roles?: Array; /** * * @type {Date} @@ -73,7 +73,7 @@ export interface OrganisationMembership { * @type {Date} * @memberof OrganisationMembership */ - deletedDate: Date; + deletedDate?: Date; } export function OrganisationMembershipFromJSON(json: any): OrganisationMembership { @@ -90,10 +90,10 @@ export function OrganisationMembershipFromJSONTyped(json: any, ignoreDiscriminat 'uuid': json['uuid'], 'userId': json['userId'], 'organisationId': json['organisationId'], - 'roles': ((json['roles'] as Array).map(MembershipRoleFromJSON)), + 'roles': !exists(json, 'roles') ? undefined : ((json['roles'] as Array).map(MembershipRoleFromJSON)), 'createdDate': (new Date(json['createdDate'])), 'updateDate': (new Date(json['updateDate'])), - 'deletedDate': (new Date(json['deletedDate'])), + 'deletedDate': !exists(json, 'deletedDate') ? undefined : (new Date(json['deletedDate'])), }; } @@ -110,10 +110,10 @@ export function OrganisationMembershipToJSON(value?: OrganisationMembership | nu 'uuid': value.uuid, 'userId': value.userId, 'organisationId': value.organisationId, - 'roles': ((value.roles as Array).map(MembershipRoleToJSON)), + 'roles': value.roles === undefined ? undefined : ((value.roles as Array).map(MembershipRoleToJSON)), 'createdDate': (value.createdDate.toISOString()), 'updateDate': (value.updateDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), + 'deletedDate': value.deletedDate === undefined ? undefined : (value.deletedDate.toISOString()), }; } diff --git a/libs/shared-api-client/src/models/OrganisationSubscriptionRecord.ts b/libs/shared-api-client/src/models/OrganisationSubscriptionRecord.ts index f449cec0..d479cccc 100644 --- a/libs/shared-api-client/src/models/OrganisationSubscriptionRecord.ts +++ b/libs/shared-api-client/src/models/OrganisationSubscriptionRecord.ts @@ -102,7 +102,7 @@ export interface OrganisationSubscriptionRecord { * @type {Date} * @memberof OrganisationSubscriptionRecord */ - deletedDate: Date; + deletedDate?: Date; } export function OrganisationSubscriptionRecordFromJSON(json: any): OrganisationSubscriptionRecord { @@ -128,7 +128,7 @@ export function OrganisationSubscriptionRecordFromJSONTyped(json: any, ignoreDis 'organisationId': json['organisationId'], 'createdDate': (new Date(json['createdDate'])), 'updatedDate': (new Date(json['updatedDate'])), - 'deletedDate': (new Date(json['deletedDate'])), + 'deletedDate': !exists(json, 'deletedDate') ? undefined : (new Date(json['deletedDate'])), }; } @@ -154,7 +154,7 @@ export function OrganisationSubscriptionRecordToJSON(value?: OrganisationSubscri 'organisationId': value.organisationId, 'createdDate': (value.createdDate.toISOString()), 'updatedDate': (value.updatedDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), + 'deletedDate': value.deletedDate === undefined ? undefined : (value.deletedDate.toISOString()), }; } diff --git a/libs/shared-api-client/src/models/User.ts b/libs/shared-api-client/src/models/User.ts index a8ff731b..b24c1d6e 100644 --- a/libs/shared-api-client/src/models/User.ts +++ b/libs/shared-api-client/src/models/User.ts @@ -101,13 +101,13 @@ export interface User { * @type {Array} * @memberof User */ - memberships: Array; + memberships?: Array; /** * * @type {Array} * @memberof User */ - apiKeys: Array; + apiKeys?: Array; /** * * @type {Date} @@ -125,7 +125,7 @@ export interface User { * @type {Date} * @memberof User */ - deletedDate: Date; + deletedDate?: Date; } export function UserFromJSON(json: any): User { @@ -149,11 +149,11 @@ export function UserFromJSONTyped(json: any, ignoreDiscriminator: boolean): User 'picture': !exists(json, 'picture') ? undefined : json['picture'], 'auth0UserId': !exists(json, 'auth0UserId') ? undefined : json['auth0UserId'], 'username': !exists(json, 'username') ? undefined : json['username'], - 'memberships': ((json['memberships'] as Array).map(OrganisationMembershipFromJSON)), - 'apiKeys': ((json['apiKeys'] as Array).map(UserApiKeyFromJSON)), + 'memberships': !exists(json, 'memberships') ? undefined : ((json['memberships'] as Array).map(OrganisationMembershipFromJSON)), + 'apiKeys': !exists(json, 'apiKeys') ? undefined : ((json['apiKeys'] as Array).map(UserApiKeyFromJSON)), 'createdDate': (new Date(json['createdDate'])), 'updateDate': (new Date(json['updateDate'])), - 'deletedDate': (new Date(json['deletedDate'])), + 'deletedDate': !exists(json, 'deletedDate') ? undefined : (new Date(json['deletedDate'])), }; } @@ -177,11 +177,11 @@ export function UserToJSON(value?: User | null): any { 'picture': value.picture, 'auth0UserId': value.auth0UserId, 'username': value.username, - 'memberships': ((value.memberships as Array).map(OrganisationMembershipToJSON)), - 'apiKeys': ((value.apiKeys as Array).map(UserApiKeyToJSON)), + 'memberships': value.memberships === undefined ? undefined : ((value.memberships as Array).map(OrganisationMembershipToJSON)), + 'apiKeys': value.apiKeys === undefined ? undefined : ((value.apiKeys as Array).map(UserApiKeyToJSON)), 'createdDate': (value.createdDate.toISOString()), 'updateDate': (value.updateDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), + 'deletedDate': value.deletedDate === undefined ? undefined : (value.deletedDate.toISOString()), }; } diff --git a/libs/shared-api-client/src/models/UserApiKey.ts b/libs/shared-api-client/src/models/UserApiKey.ts index 1d2249da..e9977740 100644 --- a/libs/shared-api-client/src/models/UserApiKey.ts +++ b/libs/shared-api-client/src/models/UserApiKey.ts @@ -66,7 +66,7 @@ export interface UserApiKey { * @type {Date} * @memberof UserApiKey */ - deletedDate: Date; + deletedDate?: Date; } export function UserApiKeyFromJSON(json: any): UserApiKey { @@ -86,7 +86,7 @@ export function UserApiKeyFromJSONTyped(json: any, ignoreDiscriminator: boolean) 'userId': json['userId'], 'createdDate': (new Date(json['createdDate'])), 'updateDate': (new Date(json['updateDate'])), - 'deletedDate': (new Date(json['deletedDate'])), + 'deletedDate': !exists(json, 'deletedDate') ? undefined : (new Date(json['deletedDate'])), }; } @@ -106,7 +106,7 @@ export function UserApiKeyToJSON(value?: UserApiKey | null): any { 'userId': value.userId, 'createdDate': (value.createdDate.toISOString()), 'updateDate': (value.updateDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), + 'deletedDate': value.deletedDate === undefined ? undefined : (value.deletedDate.toISOString()), }; } diff --git a/libs/shared-api-client/src/models/UserDto.ts b/libs/shared-api-client/src/models/UserDto.ts index 40e84af3..da38d87a 100644 --- a/libs/shared-api-client/src/models/UserDto.ts +++ b/libs/shared-api-client/src/models/UserDto.ts @@ -131,7 +131,7 @@ export interface UserDto { * @type {Date} * @memberof UserDto */ - deletedDate: Date; + deletedDate?: Date; } export function UserDtoFromJSON(json: any): UserDto { @@ -160,7 +160,7 @@ export function UserDtoFromJSONTyped(json: any, ignoreDiscriminator: boolean): U 'apiKeys': ((json['apiKeys'] as Array).map(UserApiKeyFromJSON)), 'createdDate': (new Date(json['createdDate'])), 'updateDate': (new Date(json['updateDate'])), - 'deletedDate': (new Date(json['deletedDate'])), + 'deletedDate': !exists(json, 'deletedDate') ? undefined : (new Date(json['deletedDate'])), }; } @@ -189,7 +189,7 @@ export function UserDtoToJSON(value?: UserDto | null): any { 'apiKeys': ((value.apiKeys as Array).map(UserApiKeyToJSON)), 'createdDate': (value.createdDate.toISOString()), 'updateDate': (value.updateDate.toISOString()), - 'deletedDate': (value.deletedDate.toISOString()), + 'deletedDate': value.deletedDate === undefined ? undefined : (value.deletedDate.toISOString()), }; }