Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Customer Portal #5132

Merged
merged 15 commits into from
Feb 28, 2025

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
'use client'
"use client";

import { CustomerPortal } from '@/components/CustomerPortal/CustomerPortal'
import { schemas } from '@polar-sh/client'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { CustomerPortal } from "@/components/CustomerPortal/CustomerPortal";
import { schemas } from "@polar-sh/client";
import { NuqsAdapter } from "nuqs/adapters/next/app";

const ClientPage = ({
organization,
products,
subscriptions,
orders,
customerSessionToken,
organization,
products,
subscriptions,
oneTimePurchases: orders,
customerSessionToken,
}: {
organization: schemas['Organization']
products: schemas['CustomerProduct'][]
subscriptions: schemas['ListResource_CustomerSubscription_']
orders: schemas['ListResource_CustomerOrder_']
customerSessionToken?: string
organization: schemas["Organization"];
products: schemas["CustomerProduct"][];
subscriptions: schemas["ListResource_CustomerSubscription_"];
oneTimePurchases: schemas["ListResource_CustomerOrder_"];
customerSessionToken?: string;
}) => {
return (
<NuqsAdapter>
<CustomerPortal
organization={organization}
products={products}
subscriptions={subscriptions.items ?? []}
orders={orders.items ?? []}
customerSessionToken={customerSessionToken}
/>
</NuqsAdapter>
)
}
return (
<NuqsAdapter>
<CustomerPortal
organization={organization}
products={products}
subscriptions={subscriptions.items ?? []}
oneTimePurchases={orders.items ?? []}
customerSessionToken={customerSessionToken}
/>
</NuqsAdapter>
);
};

export default ClientPage
export default ClientPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client'

import { usePostHog } from '@/hooks/posthog'
import { schemas } from '@polar-sh/client'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@polar-sh/ui/components/atoms/Select'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { twMerge } from 'tailwind-merge'

const links = (organization: schemas['Organization']) => [
{ href: `/${organization.slug}/portal/`, label: 'Overview' },
{ href: `/${organization.slug}/portal/usage/`, label: 'Usage' },
{ href: `/${organization.slug}/portal/settings/`, label: 'Settings' },
]

export const Navigation = ({
organization,
}: {
organization: schemas['Organization']
}) => {
const router = useRouter()
const currentPath = usePathname()
const searchParams = useSearchParams()
const { isFeatureEnabled } = usePostHog()

const buildPath = (path: string) => {
if (typeof window === 'undefined') {
throw new Error('Navigation is not available on the server')
}

const url = new URL(window.location.origin + currentPath)
url.pathname = path

for (const [key, value] of searchParams.entries()) {
url.searchParams.set(key, value)
}

return url.toString()
}

const filteredLinks = links(organization).filter(({ label }) =>
label === 'Usage' ? isFeatureEnabled('usage_based_billing') : true,
)

return (
<>
<nav className="hidden w-64 flex-col gap-y-1 py-12 md:flex">
{filteredLinks.map((link) => (
<Link
key={link.href}
href={buildPath(link.href)}
className={twMerge(
'dark:text-polar-500 dark:hover:bg-polar-800 rounded-xl border border-transparent px-4 py-2 font-medium text-gray-500 transition-colors duration-75 hover:bg-gray-100',
currentPath === link.href &&
'dark:bg-polar-800 dark:border-polar-700 bg-gray-100 text-black dark:text-white',
)}
>
{link.label}
</Link>
))}
</nav>
<Select
defaultValue={
filteredLinks.find(({ href }) => href === currentPath)?.label
}
onValueChange={(value) => {
router.push(
buildPath(
filteredLinks.find(({ label }) => label === value)?.href ?? '',
),
)
}}
>
<SelectTrigger className="md:hidden">
<SelectValue>
{filteredLinks.find(({ href }) => href === currentPath)?.label}
</SelectValue>
</SelectTrigger>
<SelectContent>
{filteredLinks.map((link) => (
<SelectItem key={link.href} value={link.label}>
{link.label}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)
}
29 changes: 26 additions & 3 deletions clients/apps/web/src/app/(main)/[organization]/portal/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { getServerSideAPI } from '@/utils/client/serverside'
import { getOrganizationOrNotFound } from '@/utils/customerPortal'
import Avatar from '@polar-sh/ui/components/atoms/Avatar'
import { Navigation } from './Navigation'

export default async function Layout({
params,
Expand All @@ -9,11 +11,32 @@ export default async function Layout({
children: React.ReactNode
}) {
const api = getServerSideAPI()
await getOrganizationOrNotFound(api, params.organization)
const { organization } = await getOrganizationOrNotFound(
api,
params.organization,
)

return (
<div className="dark:bg-polar-950 h-full bg-white dark:text-white">
{children}
<div className="flex flex-col">
<div className="dark:bg-polar-900 flex w-full flex-col bg-gray-50">
<div className="mx-auto flex w-full max-w-5xl flex-col justify-center gap-y-12 px-4 py-12 lg:px-0">
<div className="flex flex-row items-center gap-x-4">
<Avatar
className="h-10 w-10"
avatar_url={organization.avatar_url}
name={organization.name}
/>
<h3 className="text-lg">{organization.name}</h3>
</div>
<div>
<h2 className="text-4xl">Customer Portal</h2>
</div>
</div>
</div>
<div className="flex min-h-screen w-full flex-col items-stretch gap-6 px-4 py-8 md:mx-auto md:max-w-5xl md:flex-row md:gap-12 lg:px-0">
<Navigation organization={organization} />
<div className="flex w-full flex-col md:py-12">{children}</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default async function Page({
},
...cacheConfig,
})

const {
data: oneTimePurchases,
error: oneTimePurchasesError,
Expand Down Expand Up @@ -115,7 +116,7 @@ export default async function Page({
organization={organization}
products={products}
subscriptions={subscriptions}
orders={oneTimePurchases}
oneTimePurchases={oneTimePurchases}
customerSessionToken={searchParams.customer_session_token}
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { CustomerPortalSettings } from '@/components/CustomerPortal/CustomerPortalSettings'
import { getServerSideAPI } from '@/utils/client/serverside'
import { getOrganizationOrNotFound } from '@/utils/customerPortal'
import type { Metadata } from 'next'

export async function generateMetadata({
params,
}: {
params: { organization: string }
}): Promise<Metadata> {
const api = getServerSideAPI()
const { organization } = await getOrganizationOrNotFound(
api,
params.organization,
)

return {
title: `Customer Portal | ${organization.name}`, // " | Polar is added by the template"
openGraph: {
title: `Customer Portal | ${organization.name} on Polar`,
description: `Customer Portal | ${organization.name} on Polar`,
siteName: 'Polar',
type: 'website',
images: [
{
url: `https://polar.sh/og?org=${organization.slug}`,
width: 1200,
height: 630,
},
],
},
twitter: {
images: [
{
url: `https://polar.sh/og?org=${organization.slug}`,
width: 1200,
height: 630,
alt: `${organization.name} on Polar`,
},
],
card: 'summary_large_image',
title: `Customer Portal | ${organization.name} on Polar`,
description: `Customer Portal | ${organization.name} on Polar`,
},
}
}

export default async function Page({
params,
searchParams,
}: {
params: { organization: string }
searchParams: { customer_session_token?: string }
}) {
const api = getServerSideAPI(searchParams.customer_session_token)
const { organization } = await getOrganizationOrNotFound(
api,
params.organization,
)

return (
<CustomerPortalSettings
organization={organization}
customerSessionToken={searchParams.customer_session_token}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,13 @@ import { schemas } from '@polar-sh/client'

const ClientPage = ({
subscription,
products,
customerSessionToken,
}: {
organization: schemas['Organization']
products: schemas['CustomerProduct'][]
subscription: schemas['CustomerSubscription']
customerSessionToken?: string
}) => {
const api = createClientSideAPI(customerSessionToken)
return (
<CustomerPortalSubscription
api={api}
subscription={subscription}
products={products}
/>
)
return <CustomerPortalSubscription api={api} subscription={subscription} />
}

export default ClientPage
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default async function Page({
searchParams: { customer_session_token?: string }
}) {
const api = getServerSideAPI(searchParams.customer_session_token)
const { organization, products } = await getOrganizationOrNotFound(
const { organization } = await getOrganizationOrNotFound(
api,
params.organization,
)
Expand Down Expand Up @@ -85,8 +85,6 @@ export default async function Page({

return (
<ClientPage
organization={organization}
products={products}
subscription={subscription}
customerSessionToken={searchParams.customer_session_token}
/>
Expand Down
Loading