mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 03:32:23 +03:00
Draft of billing page
This commit is contained in:
parent
c5ed30bc55
commit
aec7cb6f38
@ -66,6 +66,7 @@ import Registration from '#/pages/authentication/Registration'
|
||||
import ResetPassword from '#/pages/authentication/ResetPassword'
|
||||
import RestoreAccount from '#/pages/authentication/RestoreAccount'
|
||||
import SetUsername from '#/pages/authentication/SetUsername'
|
||||
import * as billing from '#/pages/billing'
|
||||
import Dashboard from '#/pages/dashboard/Dashboard'
|
||||
import { Subscribe } from '#/pages/subscribe/Subscribe'
|
||||
import { SubscribeSuccess } from '#/pages/subscribe/SubscribeSuccess'
|
||||
@ -386,6 +387,16 @@ function AppRouter(props: AppRouterProps) {
|
||||
path={appUtils.DASHBOARD_PATH}
|
||||
element={shouldShowDashboard && <Dashboard {...props} />}
|
||||
/>
|
||||
<router.Route
|
||||
path={appUtils.BILLING_PATH}
|
||||
element={
|
||||
<errorBoundary.ErrorBoundary>
|
||||
<React.Suspense fallback={<loader.Loader />}>
|
||||
<billing.Billing />
|
||||
</React.Suspense>
|
||||
</errorBoundary.ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<router.Route
|
||||
path={appUtils.SUBSCRIBE_PATH}
|
||||
element={
|
||||
|
@ -32,11 +32,14 @@ export const ENTER_OFFLINE_MODE_PATH = '/offline'
|
||||
/** Path to page in which the currently active payment plan can be managed. */
|
||||
export const SUBSCRIBE_PATH = '/subscribe'
|
||||
export const SUBSCRIBE_SUCCESS_PATH = '/subscribe/success'
|
||||
/** Path to the billing page, which is used to manage the billing information */
|
||||
export const BILLING_PATH = '/billing'
|
||||
|
||||
/** A {@link RegExp} matching all paths. */
|
||||
export const ALL_PATHS_REGEX = new RegExp(
|
||||
`(?:${DASHBOARD_PATH}|${LOGIN_PATH}|${REGISTRATION_PATH}|${CONFIRM_REGISTRATION_PATH}|` +
|
||||
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH}|${RESTORE_USER_PATH}|` +
|
||||
`${ENTER_OFFLINE_MODE_PATH}|${SUBSCRIBE_PATH}|${SUBSCRIBE_SUCCESS_PATH})$`
|
||||
`${ENTER_OFFLINE_MODE_PATH}|${SUBSCRIBE_PATH}|${SUBSCRIBE_SUCCESS_PATH}|${BILLING_PATH})$`
|
||||
)
|
||||
|
||||
// ===========
|
||||
|
@ -19,7 +19,7 @@ export interface AlertProps extends React.PropsWithChildren {
|
||||
/**
|
||||
* Variants for the Alert component.
|
||||
*/
|
||||
export type AlertVariant = 'custom' | 'error' | 'info' | 'success' | 'warning'
|
||||
export type AlertVariant = 'custom' | 'error' | 'info' | 'neutral' | 'success' | 'warning'
|
||||
|
||||
/**
|
||||
* Sizes for the Alert component.
|
||||
@ -53,6 +53,7 @@ export const Alert = React.forwardRef(
|
||||
|
||||
const VARIANT_CLASSES: Record<AlertVariant, string> = {
|
||||
custom: '',
|
||||
neutral: 'bg-gray-200 border-gray-800 text-gray-800',
|
||||
error: 'bg-red-200 border-red-800 text-red-800',
|
||||
info: 'bg-blue-200 border-blue-800 text-blue-800',
|
||||
success: 'bg-green-200 border-green-800 text-green-800',
|
||||
|
@ -41,7 +41,7 @@ export interface BaseButtonProps {
|
||||
/**
|
||||
* The variant of the button
|
||||
*/
|
||||
readonly variant: Variant
|
||||
readonly variant?: Variant
|
||||
/**
|
||||
* The icon to display in the button
|
||||
*/
|
||||
@ -90,7 +90,7 @@ export type Variant =
|
||||
| 'submit'
|
||||
|
||||
const DEFAULT_CLASSES =
|
||||
'flex whitespace-nowrap cursor-pointer border border-transparent transition-[opacity,outline-offset] duration-150 ease-in-out select-none text-center items-center justify-center'
|
||||
'flex whitespace-nowrap cursor-pointer border border-transparent transition-[opacity,outline-offset] duration-150 ease-in-out select-none text-center items-center justify-center w-fit'
|
||||
const FOCUS_CLASSES =
|
||||
'focus-visible:outline-offset-2 focus:outline-none focus-visible:outline focus-visible:outline-primary'
|
||||
const EXTRA_CLICK_ZONE_CLASSES = 'flex relative before:inset-[-12px] before:absolute before:z-10'
|
||||
@ -140,7 +140,7 @@ export const Button = React.forwardRef(function Button(
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
variant = 'primary',
|
||||
icon,
|
||||
loading = false,
|
||||
isDisabled = loading,
|
||||
@ -169,18 +169,19 @@ export const Button = React.forwardRef(function Button(
|
||||
)
|
||||
|
||||
const childrenFactory = (): React.ReactNode => {
|
||||
const isIconOnly = children == null || children === '' || children === ' '
|
||||
// Icon only button
|
||||
if (variant === 'icon' && icon != null) {
|
||||
if (variant === 'icon' && icon != null && isIconOnly) {
|
||||
return (
|
||||
<aria.Text className={EXTRA_CLICK_ZONE_CLASSES}>
|
||||
<SvgMask src={icon} className="flex-none" />
|
||||
<SvgMask src={icon} className="w-[1.5em] flex-none" />
|
||||
</aria.Text>
|
||||
)
|
||||
} else {
|
||||
// Default button
|
||||
return (
|
||||
<aria.Text className={clsx('flex items-center gap-2', ICON_POSITION[iconPosition])}>
|
||||
{icon != null && <SvgMask src={icon} className="flex-none" />}
|
||||
<aria.Text className={clsx('flex items-center gap-[0.3em]', ICON_POSITION[iconPosition])}>
|
||||
{icon != null && <SvgMask src={icon} className="w-[1.2em] flex-none" />}
|
||||
<>{children}</>
|
||||
</aria.Text>
|
||||
)
|
||||
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* The separator component.
|
||||
*/
|
||||
import * as React from 'react'
|
||||
|
||||
import * as tw from 'tailwind-merge'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
|
||||
/**
|
||||
* The props for the separator component.
|
||||
*/
|
||||
export interface SeparatorProps extends aria.SeparatorProps {
|
||||
readonly className?: string
|
||||
readonly variant?: SeparatorVariant
|
||||
readonly orientation?: SeparatorOrientation
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export type SeparatorOrientation = 'horizontal' | 'vertical'
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export type SeparatorVariant = 'primary' | 'secondary'
|
||||
|
||||
const VARIANT_MAP: Record<SeparatorVariant, string> = {
|
||||
primary: 'border-primary',
|
||||
secondary: 'border-gray-500',
|
||||
}
|
||||
|
||||
/**
|
||||
* A separator component.
|
||||
*/
|
||||
export function Separator(props: SeparatorProps) {
|
||||
const { orientation = 'horizontal', variant = 'primary', className, ...rest } = props
|
||||
|
||||
return (
|
||||
<aria.Separator
|
||||
{...rest}
|
||||
orientation={orientation}
|
||||
className={tw.twMerge('isolate rounded-full', VARIANT_MAP[variant], className)}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* @file Text component
|
||||
*/
|
||||
import * as React from 'react'
|
||||
|
||||
import * as tw from 'tailwind-merge'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
import Portal from '#/components/Portal'
|
||||
|
||||
import * as mergeRefs from '#/utilities/mergeRefs'
|
||||
|
||||
/**
|
||||
* Props for the Text component
|
||||
*/
|
||||
export interface TextProps extends aria.TextProps {
|
||||
readonly variant?: TextVariant
|
||||
/**
|
||||
* Whether the text uses the monospace font.
|
||||
*/
|
||||
readonly monospace?: boolean
|
||||
/**
|
||||
* Whether the text overflow is ellipsis
|
||||
*/
|
||||
readonly ellipsis?: boolean
|
||||
/**
|
||||
* The number of lines to clamp the text to
|
||||
* Requires `ellipsis` to be `true`
|
||||
* Use `1` for single line truncation
|
||||
* Using more than 1 line will transform the text to a flex element
|
||||
* @default 1
|
||||
*/
|
||||
readonly lineClamp?: number
|
||||
/**
|
||||
* Whether the text is not wrapping
|
||||
*/
|
||||
readonly nowrap?: boolean
|
||||
/**
|
||||
* Whether the text has italic style
|
||||
*/
|
||||
readonly italic?: boolean
|
||||
|
||||
readonly weight?: Weight
|
||||
readonly transform?: TextTransform
|
||||
}
|
||||
|
||||
/**
|
||||
* Text variants
|
||||
*/
|
||||
export type TextVariant =
|
||||
| 'body'
|
||||
| 'caption'
|
||||
| 'custom'
|
||||
| 'h1'
|
||||
| 'h2'
|
||||
| 'h3'
|
||||
| 'h4'
|
||||
| 'h5'
|
||||
| 'h6'
|
||||
| 'subtitle'
|
||||
|
||||
/**
|
||||
* Font weight options
|
||||
*/
|
||||
export type Weight = 'bold' | 'custom' | 'extraBold' | 'normal' | 'thin'
|
||||
/**
|
||||
* Text transform options
|
||||
*/
|
||||
export type TextTransform = 'capitalize' | 'lowercase' | 'none' | 'uppercase'
|
||||
|
||||
const BASIC_CLASSES = 'inline-block text-balance'
|
||||
const TRUNCATE_CLASSES = 'truncate'
|
||||
const MULTILINE_TRUNCATE_CLASSES = 'line-clamp-1'
|
||||
|
||||
const VARIANT_MAP: Record<TextVariant, string> = {
|
||||
custom: '',
|
||||
body: 'text-base font-medium leading-[24px]',
|
||||
caption: 'text-sm font-medium leading-[18px]',
|
||||
h1: 'text-4xl font-bold leading-[42px]',
|
||||
h2: 'text-2xl font-semibold leading-[36px]',
|
||||
h3: 'text-xl font-semibold leading-[30px]',
|
||||
h4: 'text-lg font-medium leading-[26px]',
|
||||
h5: 'text-h5 font-medium leading-[22px]',
|
||||
h6: 'text-h6 font-medium leading-[20px]',
|
||||
subtitle: 'text-subtitle font-medium leading-[16px]',
|
||||
}
|
||||
|
||||
const WEIGHT_MAP: Record<Weight, string> = {
|
||||
bold: 'font-bold',
|
||||
extraBold: 'font-extrabold',
|
||||
normal: 'font-normal',
|
||||
thin: 'font-thin',
|
||||
custom: '',
|
||||
}
|
||||
|
||||
const TRANSFORM_MAP: Record<TextTransform, string> = {
|
||||
none: '',
|
||||
capitalize: 'text-capitalize',
|
||||
lowercase: 'text-lowercase',
|
||||
uppercase: 'text-uppercase',
|
||||
}
|
||||
|
||||
/**
|
||||
* Text component
|
||||
*/
|
||||
export const Text = React.forwardRef(function Text(
|
||||
props: TextProps,
|
||||
ref: React.Ref<HTMLSpanElement>
|
||||
) {
|
||||
const {
|
||||
className,
|
||||
variant = 'body',
|
||||
italic = false,
|
||||
weight = 'normal',
|
||||
nowrap = false,
|
||||
ellipsis = false,
|
||||
monospace = false,
|
||||
transform = 'none',
|
||||
lineClamp = 1,
|
||||
children,
|
||||
...ariaProps
|
||||
} = props
|
||||
|
||||
const textElementRef = React.useRef<HTMLElement>(null)
|
||||
const popoverRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const { hoverProps } = aria.useHover({
|
||||
onHoverStart: () => {
|
||||
if (textElementRef.current && popoverRef.current) {
|
||||
const isOverflowing =
|
||||
textElementRef.current.scrollWidth > textElementRef.current.clientWidth ||
|
||||
textElementRef.current.scrollHeight > textElementRef.current.clientHeight
|
||||
|
||||
if (isOverflowing) {
|
||||
popoverRef.current.showPopover()
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
},
|
||||
onHoverEnd: () => popoverRef.current?.hidePopover(),
|
||||
isDisabled: !ellipsis,
|
||||
})
|
||||
|
||||
const { overlayProps, updatePosition } = aria.useOverlayPosition({
|
||||
overlayRef: popoverRef,
|
||||
targetRef: textElementRef,
|
||||
})
|
||||
|
||||
const id = React.useId()
|
||||
|
||||
const classes = tw.twMerge(
|
||||
BASIC_CLASSES,
|
||||
TRANSFORM_MAP[transform],
|
||||
WEIGHT_MAP[weight],
|
||||
VARIANT_MAP[variant],
|
||||
italic && 'italic',
|
||||
nowrap && 'whitespace-nowrap',
|
||||
ellipsis && lineClamp === 1 ? TRUNCATE_CLASSES : '',
|
||||
ellipsis && lineClamp > 1 ? MULTILINE_TRUNCATE_CLASSES : '',
|
||||
monospace && 'font-mono',
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<aria.Text
|
||||
ref={mergeRefs.mergeRefs(ref, textElementRef)}
|
||||
className={classes}
|
||||
{...aria.mergeProps<TextProps>()(
|
||||
ariaProps,
|
||||
hoverProps,
|
||||
ellipsis ? { popovertarget: id, style: { WebkitLineClamp: `${lineClamp}` } } : {}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</aria.Text>
|
||||
|
||||
{ellipsis && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className={tw.twMerge(
|
||||
'inset-[unset] m-[unset] h-auto max-h-12 w-auto max-w-64 rounded-md bg-neutral-800 p-2 text-xs shadow-lg',
|
||||
'text-white'
|
||||
)}
|
||||
id={id}
|
||||
popover=""
|
||||
{...overlayProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* Barrel import file for Text component.
|
||||
*/
|
||||
export * from './Text'
|
@ -6,3 +6,5 @@ export * from './Button/Button'
|
||||
export * from './Tooltip/Tooltip'
|
||||
export * from './Dialog'
|
||||
export * from './Alert'
|
||||
export * from './Text'
|
||||
export * from './Separator'
|
||||
|
107
app/ide-desktop/lib/dashboard/src/pages/billing/Billing.tsx
Normal file
107
app/ide-desktop/lib/dashboard/src/pages/billing/Billing.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* Billing page
|
||||
*/
|
||||
import * as React from 'react'
|
||||
|
||||
import GoBack from 'enso-assets/arrow_left.svg'
|
||||
|
||||
import * as appUtils from '#/appUtils'
|
||||
|
||||
import * as text from '#/providers/TextProvider'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
|
||||
import * as components from './components'
|
||||
|
||||
/**
|
||||
* Billing page
|
||||
*/
|
||||
export function Billing() {
|
||||
const { getText } = text.useText()
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<main className="mx-auto max-w-[1024px] px-4 py-6 pb-16">
|
||||
<div className="mb-12 py-2">
|
||||
<ariaComponents.Button
|
||||
href={appUtils.DASHBOARD_PATH}
|
||||
icon={GoBack}
|
||||
iconPosition="start"
|
||||
variant="icon"
|
||||
className="-ml-3"
|
||||
size="small"
|
||||
>
|
||||
{getText('billingPageBackButton')}
|
||||
</ariaComponents.Button>
|
||||
<ariaComponents.Text variant="h1">{getText('billingPageTitle')}</ariaComponents.Text>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-12">
|
||||
<section>
|
||||
<ariaComponents.Text variant="h3">Payment Method</ariaComponents.Text>
|
||||
|
||||
<ariaComponents.Separator variant="primary" className="my-3" />
|
||||
|
||||
<components.PaymentMethod
|
||||
paymentMethods={[
|
||||
{
|
||||
id: '1',
|
||||
type: 'Mastercard',
|
||||
expMonth: 12,
|
||||
expYear: 2023,
|
||||
last4: '1234',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ariaComponents.Text variant="h3">Your plan</ariaComponents.Text>
|
||||
|
||||
<ariaComponents.Separator variant="primary" className="mb-3 mt-3" />
|
||||
|
||||
<components.YourPlan plan="enterprise" />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ariaComponents.Text variant="h3" id="BillingInvoices">
|
||||
Invoices
|
||||
</ariaComponents.Text>
|
||||
|
||||
<ariaComponents.Separator variant="primary" className="mb-3 mt-3" />
|
||||
|
||||
<components.InvoicesTable
|
||||
titleId="BillingInvoices"
|
||||
items={[
|
||||
{
|
||||
id: '1',
|
||||
date: '2021-08-01',
|
||||
amount: 100,
|
||||
status: 'Paid',
|
||||
description: 'This is a description',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2021-07-01',
|
||||
amount: 100,
|
||||
status: 'Paid',
|
||||
description: 'This is a description',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
date: '2021-06-01',
|
||||
amount: 100,
|
||||
description: 'This is a description',
|
||||
status: 'Paid',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* InvoicesCell component
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface InvoicesCellProps extends aria.CellProps {}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function InvoicesCell(props: InvoicesCellProps) {
|
||||
return <aria.Cell {...props} />
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface InvoicesColumnProps extends aria.ColumnProps {}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function InvoicesColumn(props: InvoicesColumnProps) {
|
||||
return <aria.Column {...props} />
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface InvoicesRowProps<T> extends aria.RowProps<T> {}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function InvoicesRow<T extends object>(props: InvoicesRowProps<T>) {
|
||||
return <aria.Row<T> {...props} />
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* InvoicesTable component
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
|
||||
import * as invoicesCell from './InvoicesCell'
|
||||
import * as invocesColumn from './InvoicesColumn'
|
||||
import * as invoicesRow from './InvoicesRow'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface InvoicesTableProps {
|
||||
readonly titleId: string
|
||||
readonly items: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly date: string
|
||||
readonly amount: number
|
||||
readonly status: string
|
||||
readonly description: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function InvoicesTable(props: InvoicesTableProps) {
|
||||
const { titleId, items } = props
|
||||
|
||||
return (
|
||||
<aria.ResizableTableContainer>
|
||||
<aria.Table aria-labelledby={titleId} className="text-left text-sm font-bold text-black-a50">
|
||||
<aria.TableHeader>
|
||||
<invocesColumn.InvoicesColumn
|
||||
id="symbol"
|
||||
allowsSorting
|
||||
className="h-full min-w-drive-name-column border-2 border-y border-l-0 border-transparent bg-clip-padding last:w-full last:rounded-r-full last:border-r-0"
|
||||
>
|
||||
Date
|
||||
</invocesColumn.InvoicesColumn>
|
||||
<invocesColumn.InvoicesColumn
|
||||
id="name"
|
||||
isRowHeader
|
||||
allowsSorting
|
||||
defaultWidth="3fr"
|
||||
className="h-full min-w-drive-name-column border-2 border-y border-l-0 border-transparent bg-clip-padding rounded-rows-skip-level last:w-full last:rounded-r-full last:border-r-0"
|
||||
>
|
||||
Amount
|
||||
</invocesColumn.InvoicesColumn>
|
||||
<invocesColumn.InvoicesColumn
|
||||
id="marketCap"
|
||||
allowsSorting
|
||||
className="h-full min-w-drive-name-column border-2 border-y border-l-0 border-transparent bg-clip-padding last:w-full last:rounded-r-full last:border-r-0"
|
||||
>
|
||||
Status
|
||||
</invocesColumn.InvoicesColumn>
|
||||
<invocesColumn.InvoicesColumn
|
||||
id="sector"
|
||||
allowsSorting
|
||||
className="h-full min-w-drive-name-column border-2 border-y border-l-0 border-transparent bg-clip-padding rounded-rows-skip-level last:w-full last:rounded-r-full last:border-r-0"
|
||||
>
|
||||
Description
|
||||
</invocesColumn.InvoicesColumn>
|
||||
</aria.TableHeader>
|
||||
|
||||
<aria.TableBody items={items}>
|
||||
{item => (
|
||||
<invoicesRow.InvoicesRow key={item.id} className="rounded-rows-skip-level">
|
||||
<invoicesCell.InvoicesCell>{item.date}</invoicesCell.InvoicesCell>
|
||||
<invoicesCell.InvoicesCell className="font-semibold">
|
||||
{item.amount}
|
||||
</invoicesCell.InvoicesCell>
|
||||
<invoicesCell.InvoicesCell>{item.status}</invoicesCell.InvoicesCell>
|
||||
<invoicesCell.InvoicesCell>{item.description}</invoicesCell.InvoicesCell>
|
||||
</invoicesRow.InvoicesRow>
|
||||
)}
|
||||
</aria.TableBody>
|
||||
</aria.Table>
|
||||
</aria.ResizableTableContainer>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './InvoicesTable'
|
@ -0,0 +1,67 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import Edit from 'enso-assets/pen.svg'
|
||||
|
||||
import * as text from '#/providers/TextProvider'
|
||||
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface PaymentMethodProps {
|
||||
paymentMethods: {
|
||||
id: string
|
||||
type: string
|
||||
last4: string
|
||||
expMonth: number
|
||||
expYear: number
|
||||
}[]
|
||||
onAddPaymentMethod: () => void
|
||||
onRemovePaymentMethod: (id: string) => void
|
||||
onSetDefaultPaymentMethod: (id: string) => void
|
||||
onEditPaymentMethod: (id: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function PaymentMethod(props: PaymentMethodProps) {
|
||||
const {
|
||||
paymentMethods,
|
||||
onAddPaymentMethod,
|
||||
onRemovePaymentMethod,
|
||||
onSetDefaultPaymentMethod,
|
||||
onEditPaymentMethod,
|
||||
} = props
|
||||
const { getText } = text.useText()
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{paymentMethods.map(paymentMethod => (
|
||||
<div key={paymentMethod.id} className="flex w-full flex-col py-2">
|
||||
<ariaComponents.Text>
|
||||
{getText('billingPagePaymentMethod', paymentMethod.type, paymentMethod.last4)}
|
||||
</ariaComponents.Text>
|
||||
<div>
|
||||
{getText(
|
||||
'billingPageExpires',
|
||||
paymentMethod.expMonth.toString(),
|
||||
paymentMethod.expYear.toString()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ariaComponents.Button
|
||||
variant="icon"
|
||||
size="custom"
|
||||
iconPosition="end"
|
||||
className="mt-4 text-sm"
|
||||
icon={Edit}
|
||||
>
|
||||
{getText('billingPageaddPaymentMethod')}
|
||||
</ariaComponents.Button>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import Open from 'enso-assets/open.svg'
|
||||
|
||||
import * as appUtils from '#/appUtils'
|
||||
|
||||
import * as text from '#/providers/TextProvider'
|
||||
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
|
||||
import type * as backend from '#/services/Backend'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface YourPlanProps {
|
||||
readonly plan: backend.Plan
|
||||
}
|
||||
|
||||
/**
|
||||
* YourPlan component
|
||||
*/
|
||||
export function YourPlan(props: YourPlanProps) {
|
||||
const { plan } = props
|
||||
const { getText } = text.useText()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<ariaComponents.Text variant="caption">{plan}</ariaComponents.Text>
|
||||
|
||||
<ariaComponents.Button
|
||||
variant="icon"
|
||||
size="custom"
|
||||
className="mt-4 text-sm"
|
||||
icon={Open}
|
||||
iconPosition="end"
|
||||
href={appUtils.SUBSCRIBE_PATH}
|
||||
>
|
||||
{getText('change')}
|
||||
</ariaComponents.Button>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This file is used to export all the components in the billing page
|
||||
*/
|
||||
|
||||
export * from './PaymentMethod'
|
||||
export * from './YourPlan'
|
||||
export * from './InvoicesTable'
|
6
app/ide-desktop/lib/dashboard/src/pages/billing/index.ts
Normal file
6
app/ide-desktop/lib/dashboard/src/pages/billing/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file
|
||||
*
|
||||
* Barrel file for the billing page.
|
||||
*/
|
||||
export * from './Billing'
|
@ -543,5 +543,11 @@
|
||||
"contactSalesDescription": "Contact our sales team to learn more about our Enterprise plan.",
|
||||
"ContactSalesButtonLabel": "Contact Us",
|
||||
|
||||
"setOrgNameTitle": "Set your organization name"
|
||||
"setOrgNameTitle": "Set your organization name",
|
||||
|
||||
"billingPageTitle": "Billing",
|
||||
"billingPageBackButton": "Back to Enso dashboard",
|
||||
"billingPageaddPaymentMethod": "Add payment method",
|
||||
"billingPagePaymentMethod": "$0 •••• $1",
|
||||
"billingPageExpires": "Expires $0/$1"
|
||||
}
|
||||
|
@ -89,6 +89,9 @@ interface PlaceholderOverrides {
|
||||
readonly getDefaultVersionBackendError: [string]
|
||||
|
||||
readonly subscribeSuccessSubtitle: [string]
|
||||
|
||||
readonly billingPageExpires: [string, string]
|
||||
readonly billingPagePaymentMethod: [string, string]
|
||||
}
|
||||
|
||||
/** An tuple of `string` for placeholders for each {@link TextId}. */
|
||||
|
Loading…
Reference in New Issue
Block a user