diff --git a/README.md b/README.md index 9db7b4458..edc8a010a 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,13 @@ The configuration is defined in the `apps/web/config` folder. Here you can find To install a Shadcn UI component, you can use the following command: ```bash -npx shadcn-ui@latest add --path=packages/ui/shadcn +npx shadcn-ui@latest add --path=packages/src/ui/shadcn ``` For example, to install the `Button` component, you can use the following command: ```bash -npx shadcn-ui@latest add button --path=packages/rsc/ui/shadcn +npx shadcn-ui@latest add button --path=packages/src/ui/shadcn ``` We pass the `--path` flag to specify the path where the component should be installed. \ No newline at end of file diff --git a/apps/web/README.md b/apps/web/README.md index cc4052672..ec3c44234 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,28 +1,3 @@ -# Create T3 App +# Your Application -This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. - -## What's next? How do I make an app with this? - -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. - -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. - -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Prisma](https://prisma.io) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) - -## Learn More - -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: - -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials - -You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! - -## How do I deploy this? - -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. +Write here everything about your application. diff --git a/apps/web/app/(dashboard)/home/(user)/account/layout.tsx b/apps/web/app/(dashboard)/home/(user)/account/layout.tsx index fed85d6ff..b94ce6097 100644 --- a/apps/web/app/(dashboard)/home/(user)/account/layout.tsx +++ b/apps/web/app/(dashboard)/home/(user)/account/layout.tsx @@ -1,8 +1,8 @@ -import { withI18n } from '~/lib/i18n/with-i18n'; - import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { withI18n } from '~/lib/i18n/with-i18n'; + function UserSettingsLayout(props: React.PropsWithChildren) { return ( <> diff --git a/apps/web/app/(dashboard)/home/(user)/account/page.tsx b/apps/web/app/(dashboard)/home/(user)/account/page.tsx index 151fe13b0..cac56ae21 100644 --- a/apps/web/app/(dashboard)/home/(user)/account/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/account/page.tsx @@ -1,9 +1,9 @@ +import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings'; + import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings'; - function PersonalAccountSettingsPage() { return (
}>{children}; } diff --git a/apps/web/app/(dashboard)/home/(user)/page.tsx b/apps/web/app/(dashboard)/home/(user)/page.tsx index c98360d9f..b6b2beb02 100644 --- a/apps/web/app/(dashboard)/home/(user)/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/page.tsx @@ -1,8 +1,8 @@ -import { withI18n } from '~/lib/i18n/with-i18n'; - import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { withI18n } from '~/lib/i18n/with-i18n'; + function UserHomePage() { return ( <> diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx b/apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx index c1e27fbd0..232c02042 100644 --- a/apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx +++ b/apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx @@ -1,7 +1,7 @@ -import { MobileAppNavigation } from '~/(dashboard)/home/[account]/(components)/mobile-app-navigation'; - import { PageHeader } from '@kit/ui/page'; +import { MobileAppNavigation } from '~/(dashboard)/home/[account]/(components)/mobile-app-navigation'; + export function AppHeader({ children, title, diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar-navigation.tsx b/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar-navigation.tsx index 5a4ccc1dc..b2d5e556b 100644 --- a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar-navigation.tsx +++ b/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar-navigation.tsx @@ -1,10 +1,10 @@ 'use client'; -import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config'; - import { SidebarDivider, SidebarGroup, SidebarItem } from '@kit/ui/sidebar'; import { Trans } from '@kit/ui/trans'; +import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config'; + export function AppSidebarNavigation({ account, }: React.PropsWithChildren<{ diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx b/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx index e426e1aa1..80221bab5 100644 --- a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx @@ -3,9 +3,6 @@ import { useRouter } from 'next/navigation'; import { ArrowLeftCircleIcon, ArrowRightCircleIcon } from 'lucide-react'; -import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown'; -import featureFlagsConfig from '~/config/feature-flags.config'; -import pathsConfig from '~/config/paths.config'; import { AccountSelector } from '@kit/accounts/account-selector'; import { Sidebar, SidebarContent } from '@kit/ui/sidebar'; @@ -18,6 +15,10 @@ import { import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; +import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown'; +import featureFlagsConfig from '~/config/feature-flags.config'; +import pathsConfig from '~/config/paths.config'; + import { AppSidebarNavigation } from './app-sidebar-navigation'; type AccountModel = { diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/mobile-app-navigation.tsx b/apps/web/app/(dashboard)/home/[account]/(components)/mobile-app-navigation.tsx index fdb839c7c..7e65eafcc 100644 --- a/apps/web/app/(dashboard)/home/[account]/(components)/mobile-app-navigation.tsx +++ b/apps/web/app/(dashboard)/home/[account]/(components)/mobile-app-navigation.tsx @@ -4,9 +4,6 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { HomeIcon, LogOutIcon, MenuIcon } from 'lucide-react'; -import featureFlagsConfig from '~/config/feature-flags.config'; -import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config'; -import pathsConfig from '~/config/paths.config'; import { AccountSelector } from '@kit/accounts/account-selector'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; @@ -26,6 +23,10 @@ import { } from '@kit/ui/dropdown-menu'; import { Trans } from '@kit/ui/trans'; +import featureFlagsConfig from '~/config/feature-flags.config'; +import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config'; +import pathsConfig from '~/config/paths.config'; + export const MobileAppNavigation = ( props: React.PropsWithChildren<{ slug: string; diff --git a/apps/web/app/(dashboard)/home/[account]/(lib)/load-workspace.ts b/apps/web/app/(dashboard)/home/[account]/(lib)/load-workspace.ts index 0fc2475a3..865a5cd1e 100644 --- a/apps/web/app/(dashboard)/home/[account]/(lib)/load-workspace.ts +++ b/apps/web/app/(dashboard)/home/[account]/(lib)/load-workspace.ts @@ -3,10 +3,11 @@ import { cache } from 'react'; import { redirect } from 'next/navigation'; import 'server-only'; -import pathsConfig from '~/config/paths.config'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import pathsConfig from '~/config/paths.config'; + /** * Load the organization workspace data. * We place this function into a separate file so it can be reused in multiple places across the server components. diff --git a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx index 999fd08ee..858dd39af 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx @@ -1,8 +1,8 @@ -import { withI18n } from '~/lib/i18n/with-i18n'; - import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { withI18n } from '~/lib/i18n/with-i18n'; + function OrganizationAccountBillingPage() { return ( <> diff --git a/apps/web/app/(dashboard)/home/[account]/layout.tsx b/apps/web/app/(dashboard)/home/[account]/layout.tsx index 4d67abd03..83a76c332 100644 --- a/apps/web/app/(dashboard)/home/[account]/layout.tsx +++ b/apps/web/app/(dashboard)/home/[account]/layout.tsx @@ -1,9 +1,9 @@ -import { withI18n } from '~/lib/i18n/with-i18n'; - import { parseSidebarStateCookie } from '@kit/shared/cookies/sidebar-state.cookie'; import { parseThemeCookie } from '@kit/shared/cookies/theme.cookie'; import { Page } from '@kit/ui/page'; +import { withI18n } from '~/lib/i18n/with-i18n'; + import { AppSidebar } from './(components)/app-sidebar'; import { loadOrganizationWorkspace } from './(lib)/load-workspace'; diff --git a/apps/web/app/(dashboard)/home/[account]/members/page.tsx b/apps/web/app/(dashboard)/home/[account]/members/page.tsx index 49d8a4259..db207f8e9 100644 --- a/apps/web/app/(dashboard)/home/[account]/members/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/members/page.tsx @@ -1,6 +1,4 @@ import { PlusCircledIcon } from '@radix-ui/react-icons'; -import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace'; -import { withI18n } from '~/lib/i18n/with-i18n'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { @@ -19,6 +17,9 @@ import { import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace'; +import { withI18n } from '~/lib/i18n/with-i18n'; + interface Params { params: { account: string; diff --git a/apps/web/app/(dashboard)/home/[account]/page.tsx b/apps/web/app/(dashboard)/home/[account]/page.tsx index 64332296d..239328a39 100644 --- a/apps/web/app/(dashboard)/home/[account]/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/page.tsx @@ -1,14 +1,15 @@ import loadDynamic from 'next/dynamic'; import { PlusIcon } from 'lucide-react'; -import { AppHeader } from '~/(dashboard)/home/[account]/(components)/app-header'; -import { withI18n } from '~/lib/i18n/with-i18n'; import { Button } from '@kit/ui/button'; import { PageBody } from '@kit/ui/page'; import Spinner from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; +import { AppHeader } from '~/(dashboard)/home/[account]/(components)/app-header'; +import { withI18n } from '~/lib/i18n/with-i18n'; + const DashboardDemo = loadDynamic( () => import('~/(dashboard)/home/[account]/(components)/dashboard-demo'), { diff --git a/apps/web/app/(dashboard)/home/[account]/settings/loading.tsx b/apps/web/app/(dashboard)/home/[account]/settings/loading.tsx deleted file mode 100644 index 4ea53181d..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { GlobalLoader } from '@kit/ui/global-loader'; - -export default GlobalLoader; diff --git a/apps/web/app/(dashboard)/home/[account]/settings/page.tsx b/apps/web/app/(dashboard)/home/[account]/settings/page.tsx deleted file mode 100644 index 0d80b9653..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { use } from 'react'; - -import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace'; -import featureFlagsConfig from '~/config/feature-flags.config'; -import { withI18n } from '~/lib/i18n/with-i18n'; - -import { - TeamAccountDangerZone, - UpdateOrganizationForm, -} from '@kit/team-accounts/components'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@kit/ui/card'; -import { If } from '@kit/ui/if'; -import { PageBody, PageHeader } from '@kit/ui/page'; -import { Trans } from '@kit/ui/trans'; - -export const metadata = { - title: 'Organization Settings', -}; - -const allowOrganizationDelete = featureFlagsConfig.enableOrganizationDeletion; - -interface Params { - params: { - account: string; - }; -} - -function OrganizationSettingsPage({ params }: Params) { - const { account, user } = use(loadOrganizationWorkspace(params.account)); - - return ( - <> - } - description={} - /> - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - ); -} - -export default withI18n(OrganizationSettingsPage); diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/billing-redirect-button.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/billing-redirect-button.tsx deleted file mode 100644 index d8e6fce0e..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/billing-redirect-button.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { ArrowUpRightIcon } from 'lucide-react'; - -import { Button } from '@kit/ui/button'; - -export function BillingPortalRedirectButton({ - children, - customerId, - className, -}: React.PropsWithChildren<{ - customerId: string; - className?: string; -}>) { - return ( -
- - - -
- ); -} diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/checkout-redirect-button.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/checkout-redirect-button.tsx deleted file mode 100644 index 17d340d3b..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/checkout-redirect-button.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -import { useFormState, useFormStatus } from 'react-dom'; - -import { ChevronRightIcon } from 'lucide-react'; - -import { isBrowser } from '@kit/shared/utils'; -import { Button } from '@kit/ui/button'; -import { cn } from '@kit/ui/utils'; - -export function CheckoutRedirectButton({ - children, - onCheckoutCreated, - ...props -}): React.PropsWithChildren<{ - disabled?: boolean; - stripePriceId?: string; - recommended?: boolean; - organizationUid: string; - onCheckoutCreated?: (clientSecret: string) => void; -}> { - const [state, formAction] = useFormState(createCheckoutAction, { - clientSecret: '', - }); - - useEffect(() => { - if (state.clientSecret && onCheckoutCreated) { - onCheckoutCreated(state.clientSecret); - } - }, [state.clientSecret, onCheckoutCreated]); - - return ( -
- - - - {children} - - - ); -} - -function SubmitCheckoutButton( - props: React.PropsWithChildren<{ - recommended?: boolean; - disabled?: boolean; - }>, -) { - const { pending } = useFormStatus(); - - return ( - - ); -} - -function CheckoutFormData( - props: React.PropsWithChildren<{ - organizationUid: string | undefined; - priceId: string | undefined; - }>, -) { - return ( - <> - - - - - - ); -} - -function getReturnUrl() { - return isBrowser() - ? [window.location.origin, window.location.pathname].join('') - : undefined; -} diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/embedded-stripe-checkout.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/embedded-stripe-checkout.tsx deleted file mode 100644 index ee68b79cb..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/embedded-stripe-checkout.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { Close as DialogPrimitiveClose } from '@radix-ui/react-dialog'; -import { - EmbeddedCheckout, - EmbeddedCheckoutProvider, -} from '@stripe/react-stripe-js'; -import { loadStripe } from '@stripe/stripe-js'; -import { XIcon } from 'lucide-react'; - -import pricingConfig, { - StripeCheckoutDisplayMode, -} from '@/config/pricing.config'; - -import { cn } from '@/lib/utils'; - -import If from '@/components/app/If'; -import LogoImage from '@/components/app/Logo/LogoImage'; -import Trans from '@/components/app/Trans'; - -const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; - -if (!STRIPE_PUBLISHABLE_KEY) { - throw new Error( - 'Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY environment variable. Did you forget to add it to your .env file?', - ); -} - -const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY); - -export default function EmbeddedStripeCheckout({ - clientSecret, - onClose, -}: React.PropsWithChildren<{ - clientSecret: string; - onClose?: () => void; -}>) { - return ( - - - - - - ); -} - -function EmbeddedCheckoutPopup({ - onClose, - children, -}: React.PropsWithChildren<{ - onClose?: () => void; -}>) { - const [open, setOpen] = useState(true); - - const displayMode = pricingConfig.displayMode; - const isPopup = displayMode === StripeCheckoutDisplayMode.Popup; - const isOverlay = displayMode === StripeCheckoutDisplayMode.Overlay; - - const className = cn({ - [`bg-white p-4 max-h-[98vh] overflow-y-auto shadow-transparent border border-gray-200 dark:border-dark-700`]: - isPopup, - [`bg-background !flex flex-col flex-1 fixed top-0 !max-h-full !max-w-full left-0 w-screen h-screen border-transparent shadow-transparent py-4 px-8`]: - isOverlay, - }); - - const close = () => { - setOpen(false); - - if (onClose) { - onClose(); - } - }; - - return ( - { - if (!open && onClose) { - onClose(); - } - - setOpen(open); - }} - > - e.preventDefault()} - onInteractOutside={(e) => e.preventDefault()} - > - -
-
- - - -
-
-
- - - - - - - -
- {children} -
-
-
- ); -} diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plan-selection-form.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plan-selection-form.tsx deleted file mode 100644 index fd2acfcb2..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plan-selection-form.tsx +++ /dev/null @@ -1,112 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; - -import dynamic from 'next/dynamic'; - -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; - -import type Organization from '@/lib/organizations/types/organization'; - -import ErrorBoundary from '@/components/app/ErrorBoundary'; -import If from '@/components/app/If'; -import PricingTable from '@/components/app/PricingTable'; -import Trans from '@/components/app/Trans'; - -import BillingPortalRedirectButton from './billing-redirect-button'; -import CheckoutRedirectButton from './checkout-redirect-button'; - -const EmbeddedStripeCheckout = dynamic( - () => import('./embedded-stripe-checkout'), - { - ssr: false, - }, -); - -const PlanSelectionForm: React.FC<{ - organization: WithId; - customerId: Maybe; -}> = ({ organization, customerId }) => { - const [clientSecret, setClientSecret] = useState(); - const [retry, setRetry] = useState(0); - - return ( -
- - - - -
- { - return ( - setRetry((retry) => retry + 1)} - /> - } - > - - - - - ); - }} - /> - - -
- - - - - - - -
-
-
-
- ); -}; - -export default PlanSelectionForm; - -function NoPermissionsAlert() { - return ( - - - - - - - - - - ); -} - -function CheckoutErrorMessage({ onRetry }: { onRetry: () => void }) { - return ( -
- - - - - -
- ); -} diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plan-status-alert-container.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plan-status-alert-container.tsx deleted file mode 100644 index 027ae129f..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plan-status-alert-container.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client'; - -import React from 'react'; - -import type { ReadonlyURLSearchParams } from 'next/navigation'; -import { useSearchParams } from 'next/navigation'; - -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; - -import Trans from '@/components/app/Trans'; - -enum SubscriptionStatusQueryParams { - Success = 'success', - Cancel = 'cancel', - Error = 'error', -} - -function PlansStatusAlertContainer() { - const status = useSubscriptionStatus(); - - if (status === undefined) { - return null; - } - - return ; -} - -export default PlansStatusAlertContainer; - -function PlansStatusAlert({ - status, -}: { - status: SubscriptionStatusQueryParams; -}) { - switch (status) { - case SubscriptionStatusQueryParams.Cancel: - return ( - - - - - - - - - - ); - - case SubscriptionStatusQueryParams.Error: - return ( - - - - - - - - - - ); - - case SubscriptionStatusQueryParams.Success: - return ( - - - - - - - - - - ); - } -} - -function useSubscriptionStatus() { - const params = useSearchParams(); - - return getStatus(params); -} - -function getStatus(params: ReadonlyURLSearchParams | null) { - if (!params) { - return; - } - - const error = params.has(SubscriptionStatusQueryParams.Error); - const canceled = params.has(SubscriptionStatusQueryParams.Cancel); - const success = params.has(SubscriptionStatusQueryParams.Success); - - if (canceled) { - return SubscriptionStatusQueryParams.Cancel; - } else if (success) { - return SubscriptionStatusQueryParams.Success; - } else if (error) { - return SubscriptionStatusQueryParams.Error; - } -} diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plans-container.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plans-container.tsx deleted file mode 100644 index 9527e86a6..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/plans-container.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import useCurrentOrganization from '@/lib/organizations/hooks/use-current-organization'; - -import If from '@/components/app/If'; -import Trans from '@/components/app/Trans'; - -import BillingPortalRedirectButton from './billing-redirect-button'; -import PlanSelectionForm from './plan-selection-form'; -import SubscriptionCard from './subscription-card'; - -const PlansContainer: React.FC = () => { - const organization = useCurrentOrganization(); - - if (!organization) { - return null; - } - - const customerId = organization.subscription?.customerId; - const subscription = organization.subscription?.data; - - if (!subscription) { - return ( - - ); - } - - return ( -
-
-
-
- -
- - -
-
- - - - - - - -
-
-
-
-
-
- ); -}; - -export default PlansContainer; diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/subscription-card.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/subscription-card.tsx deleted file mode 100644 index fb65517f7..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/subscription-card.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useMemo } from 'react'; - -import Heading from '@/components/ui/heading'; -import { CheckCircleIcon, XCircleIcon } from 'lucide-react'; -import { getI18n } from 'react-i18next'; -import SubscriptionStatusBadge from '~/(dashboard)/home/[account]/(components)/organizations/SubscriptionStatusBadge'; - -import pricingConfig from '@/config/pricing.config'; - -import type { OrganizationSubscription } from '@/lib/organizations/types/organization-subscription'; - -import If from '@/components/app/If'; -import PricingTable from '@/components/app/PricingTable'; -import Trans from '@/components/app/Trans'; - -import SubscriptionStatusAlert from './subscription-status-alert'; - -const SubscriptionCard: React.FC<{ - subscription: OrganizationSubscription; -}> = ({ subscription }) => { - const details = useSubscriptionDetails(subscription.priceId); - const cancelAtPeriodEnd = subscription.cancelAtPeriodEnd; - const isActive = subscription.status === 'active'; - const language = getI18n().language; - - const dates = useMemo(() => { - const endDate = new Date(subscription.periodEndsAt); - const trialEndDate = - subscription.trialEndsAt && new Date(subscription.trialEndsAt); - - return { - endDate: endDate.toLocaleDateString(language), - trialEndDate: trialEndDate - ? trialEndDate.toLocaleDateString(language) - : null, - }; - }, [language, subscription]); - - if (!details) { - return null; - } - - return ( -
-
-
-
- - - {details.product.name} - - - -
- -
-
- - - {details.product.description} - -
- - - - - - -
- -
- - {details.plan.price} - - - /{details.plan.name} - - -
-
- ); -}; - -function RenewStatusDescription( - props: React.PropsWithChildren<{ - cancelAtPeriodEnd: boolean; - dates: { - endDate: string; - trialEndDate: string | null; - }; - }>, -) { - return ( - - - - - - - - - - - - - - - - - - ); -} - -function useSubscriptionDetails(priceId: string) { - const products = pricingConfig.products; - - return useMemo(() => { - for (const product of products) { - for (const plan of product.plans) { - if (plan.stripePriceId === priceId) { - return { plan, product }; - } - } - } - }, [products, priceId]); -} - -export default SubscriptionCard; diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/subscription-status-alert.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/subscription-status-alert.tsx deleted file mode 100644 index 492e98364..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/components/subscription-status-alert.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import classNames from 'clsx'; - -import type { OrganizationSubscription } from '@/lib/organizations/types/organization-subscription'; - -import Trans from '@/components/app/Trans'; - -function SubscriptionStatusAlert( - props: React.PropsWithChildren<{ - subscription: OrganizationSubscription; - values: { - endDate: string; - trialEndDate: string | null; - }; - }>, -) { - const status = props.subscription.status; - - let message = ''; - let type: 'success' | 'error' | 'warn'; - - switch (status) { - case 'active': - message = 'subscription:status.active.description'; - type = 'success'; - break; - case 'trialing': - message = 'subscription:status.trialing.description'; - type = 'success'; - break; - case 'canceled': - message = 'subscription:status.canceled.description'; - type = 'warn'; - break; - case 'incomplete': - message = 'subscription:status.incomplete.description'; - type = 'warn'; - break; - case 'incomplete_expired': - message = 'subscription:status.incomplete_expired.description'; - type = 'error'; - break; - case 'unpaid': - message = 'subscription:status.unpaid.description'; - type = 'error'; - break; - case 'past_due': - message = 'subscription:status.past_due.description'; - type = 'error'; - - break; - default: - return null; - } - - return ( - - - - ); -} - -export default SubscriptionStatusAlert; diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/page.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/page.tsx deleted file mode 100644 index a54ae5929..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import Heading from '@/components/ui/heading'; - -import { withI18n } from '@packages/i18n/with-i18n'; - -import Trans from '@/components/app/Trans'; - -import PlansStatusAlertContainer from './components/plan-status-alert-container'; -import PlansContainer from './components/plans-container'; - -export const metadata = { - title: 'Subscription', -}; - -const SubscriptionSettingsPage = () => { - return ( -
-
- - - - - - - -
- - - - -
- ); -}; - -export default withI18n(SubscriptionSettingsPage); diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx index 16e071c72..4be6a9527 100644 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx +++ b/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx @@ -4,12 +4,13 @@ import Link from 'next/link'; import { CheckIcon, ChevronRightIcon } from 'lucide-react'; import type { Stripe } from 'stripe'; -import pathsConfig from '~/config/paths.config'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; +import pathsConfig from '~/config/paths.config'; + /** * Retrieves the session status for a Stripe checkout session. * Since we should only arrive here for a successful checkout, we only check diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx index 30f51601f..0ea55ff44 100644 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx @@ -1,12 +1,12 @@ import { notFound, redirect } from 'next/navigation'; +import requireSession from '@/lib/user/require-session'; + import { withI18n } from '@packages/i18n/with-i18n'; import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; import createStripeClient from '@kit/stripe/get-stripe'; -import requireSession from '@/lib/user/require-session'; - import { BillingSessionStatus } from './components/billing-session-status'; import RecoverCheckout from './components/recover-checkout'; diff --git a/apps/web/app/(dashboard)/home/components/home-sidebar-account-selector.tsx b/apps/web/app/(dashboard)/home/components/home-sidebar-account-selector.tsx index 693284ada..14806229c 100644 --- a/apps/web/app/(dashboard)/home/components/home-sidebar-account-selector.tsx +++ b/apps/web/app/(dashboard)/home/components/home-sidebar-account-selector.tsx @@ -2,11 +2,11 @@ import { useRouter } from 'next/navigation'; +import { AccountSelector } from '@kit/accounts/account-selector'; + import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; -import { AccountSelector } from '@kit/accounts/account-selector'; - const features = { enableOrganizationAccounts: featureFlagsConfig.enableOrganizationAccounts, enableOrganizationCreation: featureFlagsConfig.enableOrganizationCreation, diff --git a/apps/web/app/(dashboard)/home/components/home-sidebar.tsx b/apps/web/app/(dashboard)/home/components/home-sidebar.tsx index be4986139..8912b9d60 100644 --- a/apps/web/app/(dashboard)/home/components/home-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/components/home-sidebar.tsx @@ -2,13 +2,13 @@ import { use } from 'react'; import { cookies } from 'next/headers'; +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar'; + import { HomeSidebarAccountSelector } from '~/(dashboard)/home/components/home-sidebar-account-selector'; import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown'; import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config'; -import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; -import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar'; - export function HomeSidebar() { const collapsed = getSidebarCollapsed(); const accounts = use(loadUserAccounts()); diff --git a/apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx b/apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx index 9c5870ea1..c111125d2 100644 --- a/apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx +++ b/apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx @@ -1,11 +1,11 @@ 'use client'; -import pathsConfig from '~/config/paths.config'; - import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useUserSession } from '@kit/supabase/hooks/use-user-session'; +import pathsConfig from '~/config/paths.config'; + export function ProfileDropdownContainer(props: { collapsed: boolean }) { const userSession = useUserSession(); const signOut = useSignOut(); diff --git a/apps/web/app/(marketing)/blog/[slug]/page.tsx b/apps/web/app/(marketing)/blog/[slug]/page.tsx index 55f6a2b88..32f0989c8 100644 --- a/apps/web/app/(marketing)/blog/[slug]/page.tsx +++ b/apps/web/app/(marketing)/blog/[slug]/page.tsx @@ -4,6 +4,7 @@ import { notFound } from 'next/navigation'; import Script from 'next/script'; import { allPosts } from 'contentlayer/generated'; + import appConfig from '~/config/app.config'; import { withI18n } from '~/lib/i18n/with-i18n'; diff --git a/apps/web/app/(marketing)/blog/components/post-header.tsx b/apps/web/app/(marketing)/blog/components/post-header.tsx index 5247073ad..516ab71bc 100644 --- a/apps/web/app/(marketing)/blog/components/post-header.tsx +++ b/apps/web/app/(marketing)/blog/components/post-header.tsx @@ -1,10 +1,11 @@ import type { Post } from 'contentlayer/generated'; -import { CoverImage } from '~/(marketing)/blog/components/cover-image'; -import { DateFormatter } from '~/(marketing)/blog/components/date-formatter'; import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; +import { CoverImage } from '~/(marketing)/blog/components/cover-image'; +import { DateFormatter } from '~/(marketing)/blog/components/date-formatter'; + const PostHeader: React.FC<{ post: Post; }> = ({ post }) => { diff --git a/apps/web/app/(marketing)/blog/components/post-preview.tsx b/apps/web/app/(marketing)/blog/components/post-preview.tsx index a151f1056..3b42e786b 100644 --- a/apps/web/app/(marketing)/blog/components/post-preview.tsx +++ b/apps/web/app/(marketing)/blog/components/post-preview.tsx @@ -1,11 +1,12 @@ import Link from 'next/link'; import type { Post } from 'contentlayer/generated'; -import { CoverImage } from '~/(marketing)/blog/components/cover-image'; -import { DateFormatter } from '~/(marketing)/blog/components/date-formatter'; import { If } from '@kit/ui/if'; +import { CoverImage } from '~/(marketing)/blog/components/cover-image'; +import { DateFormatter } from '~/(marketing)/blog/components/date-formatter'; + type Props = { post: Post; preloadImage?: boolean; diff --git a/apps/web/app/(marketing)/blog/page.tsx b/apps/web/app/(marketing)/blog/page.tsx index 38f1976f4..ff1f37569 100644 --- a/apps/web/app/(marketing)/blog/page.tsx +++ b/apps/web/app/(marketing)/blog/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { allPosts } from 'contentlayer/generated'; + import PostPreview from '~/(marketing)/blog/components/post-preview'; import { SitePageHeader } from '~/(marketing)/components/site-page-header'; import appConfig from '~/config/app.config'; diff --git a/apps/web/app/(marketing)/components/site-header-account-section.tsx b/apps/web/app/(marketing)/components/site-header-account-section.tsx index 1d8d3af28..8870bd103 100644 --- a/apps/web/app/(marketing)/components/site-header-account-section.tsx +++ b/apps/web/app/(marketing)/components/site-header-account-section.tsx @@ -3,13 +3,14 @@ import Link from 'next/link'; import { ChevronRightIcon } from 'lucide-react'; -import pathsConfig from '~/config/paths.config'; import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useUserSession } from '@kit/supabase/hooks/use-user-session'; import { Button } from '@kit/ui/button'; +import pathsConfig from '~/config/paths.config'; + export function SiteHeaderAccountSection() { const signOut = useSignOut(); const userSession = useUserSession(); diff --git a/apps/web/app/(marketing)/docs/[...slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx index 5d9c4f567..e7a1b1323 100644 --- a/apps/web/app/(marketing)/docs/[...slug]/page.tsx +++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx @@ -4,15 +4,16 @@ import { notFound } from 'next/navigation'; import { allDocumentationPages } from 'contentlayer/generated'; import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; + +import { If } from '@kit/ui/if'; +import { Mdx } from '@kit/ui/mdx'; + import { SitePageHeader } from '~/(marketing)/components/site-page-header'; import { DocsCards } from '~/(marketing)/docs/components/docs-cards'; import { DocumentationPageLink } from '~/(marketing)/docs/components/documentation-page-link'; import { getDocumentationPageTree } from '~/(marketing)/docs/utils/get-documentation-page-tree'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { If } from '@kit/ui/if'; -import { Mdx } from '@kit/ui/mdx'; - const getPageBySlug = cache((slug: string) => { return allDocumentationPages.find((post) => post.resolvedPath === slug); }); diff --git a/apps/web/app/(marketing)/docs/page.tsx b/apps/web/app/(marketing)/docs/page.tsx index 7338a4d84..265448225 100644 --- a/apps/web/app/(marketing)/docs/page.tsx +++ b/apps/web/app/(marketing)/docs/page.tsx @@ -1,4 +1,5 @@ import { allDocumentationPages } from 'contentlayer/generated'; + import appConfig from '~/config/app.config'; import { withI18n } from '~/lib/i18n/with-i18n'; diff --git a/apps/web/app/(marketing)/faq/page.tsx b/apps/web/app/(marketing)/faq/page.tsx index 59f9a2a57..c4c62d7b7 100644 --- a/apps/web/app/(marketing)/faq/page.tsx +++ b/apps/web/app/(marketing)/faq/page.tsx @@ -1,4 +1,5 @@ import { ChevronDownIcon } from 'lucide-react'; + import { withI18n } from '~/lib/i18n/with-i18n'; import { SitePageHeader } from '../components/site-page-header'; diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx index 84a2fb8c2..0c54e53d4 100644 --- a/apps/web/app/(marketing)/page.tsx +++ b/apps/web/app/(marketing)/page.tsx @@ -2,11 +2,12 @@ import Image from 'next/image'; import Link from 'next/link'; import { ChevronRightIcon } from 'lucide-react'; -import { withI18n } from '~/lib/i18n/with-i18n'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; +import { withI18n } from '~/lib/i18n/with-i18n'; + function Home() { return (
diff --git a/apps/web/app/(marketing)/pricing/page.tsx b/apps/web/app/(marketing)/pricing/page.tsx index 6d65111ea..df15bbd1d 100644 --- a/apps/web/app/(marketing)/pricing/page.tsx +++ b/apps/web/app/(marketing)/pricing/page.tsx @@ -1,9 +1,9 @@ +import { PricingTable } from '@kit/billing/components/pricing-table'; + import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { PricingTable } from '@kit/billing/components/pricing-table'; - import { SitePageHeader } from '../components/site-page-header'; export const metadata = { diff --git a/apps/web/app/admin/lib/actions-utils.ts b/apps/web/app/admin/lib/actions-utils.ts index e42f64a4d..fd4d68fbe 100644 --- a/apps/web/app/admin/lib/actions-utils.ts +++ b/apps/web/app/admin/lib/actions-utils.ts @@ -1,9 +1,9 @@ import { notFound } from 'next/navigation'; -import isUserSuperAdmin from '~/admin/utils/is-user-super-admin'; - import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; +import isUserSuperAdmin from '~/admin/utils/is-user-super-admin'; + export function withAdminSession( fn: (...params: Args) => Response, ) { diff --git a/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts b/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts index e45dd72bf..043de7538 100644 --- a/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts +++ b/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts @@ -3,11 +3,11 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { withAdminSession } from '~/admin/lib/actions-utils'; - import { Logger } from '@kit/shared/logger'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; +import { withAdminSession } from '~/admin/lib/actions-utils'; + const getClient = () => getSupabaseServerActionClient({ admin: true }); export const deleteOrganizationAction = withAdminSession( diff --git a/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx b/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx index b3f0580eb..ec1d00673 100644 --- a/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx +++ b/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx @@ -4,6 +4,8 @@ import { useState, useTransition } from 'react'; import { useRouter } from 'next/navigation'; +import type Organization from '@/lib/organizations/types/organization'; + import useCsrfToken from '@kit/hooks/use-csrf-token'; import { Button } from '@kit/ui/button'; import { @@ -15,8 +17,6 @@ import { import { Input } from '@kit/ui/input'; import { Label } from '@kit/ui/label'; -import type Organization from '@/lib/organizations/types/organization'; - import { deleteOrganizationAction } from '../actions.server'; function DeleteOrganizationModal({ diff --git a/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx b/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx index c3066801e..c3c63899f 100644 --- a/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx +++ b/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx @@ -1,7 +1,7 @@ -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - import { getOrganizationByUid } from '@/lib/organizations/database/queries'; +import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; + import AdminGuard from '../../../../../../packages/admin/components/AdminGuard'; import DeleteOrganizationModal from '../components/DeleteOrganizationModal'; diff --git a/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx b/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx index 8e6dff7ea..4ac0a5e55 100644 --- a/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx +++ b/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx @@ -3,6 +3,8 @@ import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; +import { DataTable } from '@/components/app/DataTable'; +import type Membership from '@/lib/organizations/types/membership'; import type { ColumnDef } from '@tanstack/react-table'; import { EllipsisVerticalIcon } from 'lucide-react'; @@ -15,10 +17,6 @@ import { DropdownMenuTrigger, } from '@kit/ui/dropdown-menu'; -import type Membership from '@/lib/organizations/types/membership'; - -import { DataTable } from '@/components/app/DataTable'; - import RoleBadge from '../../../../../(app)/[account]/account/organization/components/RoleBadge'; type Data = { diff --git a/apps/web/app/admin/organizations/[uid]/members/page.tsx b/apps/web/app/admin/organizations/[uid]/members/page.tsx index 69ff02eb2..7fdc0b022 100644 --- a/apps/web/app/admin/organizations/[uid]/members/page.tsx +++ b/apps/web/app/admin/organizations/[uid]/members/page.tsx @@ -2,15 +2,13 @@ import { use } from 'react'; import Link from 'next/link'; +import { PageBody } from '@/components/app/Page'; +import appConfig from '@/config/app.config'; import { ChevronRightIcon } from 'lucide-react'; import AdminHeader from '@packages/admin/components/AdminHeader'; import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; -import appConfig from '@/config/app.config'; - -import { PageBody } from '@/components/app/Page'; - import getPageFromQueryParams from '../../../utils/get-page-from-query-param'; import { getMembershipsByOrganizationUid } from '../../queries'; import OrganizationsMembersTable from './components/OrganizationsMembersTable'; diff --git a/apps/web/app/admin/organizations/components/OrganizationsTable.tsx b/apps/web/app/admin/organizations/components/OrganizationsTable.tsx deleted file mode 100644 index 78d20b8d9..000000000 --- a/apps/web/app/admin/organizations/components/OrganizationsTable.tsx +++ /dev/null @@ -1,198 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -import type { ColumnDef } from '@tanstack/react-table'; -import { EllipsisIcon } from 'lucide-react'; -import { getI18n } from 'react-i18next'; - -import { Button } from '@kit/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@kit/ui/dropdown-menu'; - -import pricingConfig from '@/config/pricing.config'; - -import { DataTable } from '@/components/app/DataTable'; - -import SubscriptionStatusBadge from '../../../(app)/[account]/components/organizations/SubscriptionStatusBadge'; -import type { getOrganizations } from '../queries'; - -type Response = Awaited>; -type Organizations = Response['organizations']; - -const columns: ColumnDef[] = [ - { - header: 'ID', - accessorKey: 'id', - id: 'id', - size: 10, - }, - { - header: 'UUID', - accessorKey: 'uuid', - id: 'uuid', - size: 200, - }, - { - header: 'Name', - accessorKey: 'name', - id: 'name', - }, - { - header: 'Subscription', - id: 'subscription', - cell: ({ row }) => { - const priceId = row.original?.subscription?.data?.priceId; - - const plan = pricingConfig.products.find((product) => { - return product.plans.some((plan) => plan.stripePriceId === priceId); - }); - - if (plan) { - const price = plan.plans.find((plan) => plan.stripePriceId === priceId); - - if (!price) { - return 'Unknown Price'; - } - - return `${plan.name} - ${price.name}`; - } - - return '-'; - }, - }, - { - header: 'Subscription Status', - id: 'subscription-status', - cell: ({ row }) => { - const subscription = row.original?.subscription?.data; - - if (!subscription) { - return '-'; - } - - return ; - }, - }, - { - header: 'Subscription Period', - id: 'subscription-period', - cell: ({ row }) => { - const subscription = row.original?.subscription?.data; - const i18n = getI18n(); - const language = i18n.language ?? 'en'; - - if (!subscription) { - return '-'; - } - - const canceled = subscription.cancelAtPeriodEnd; - const date = subscription.periodEndsAt; - const formattedDate = new Date(date).toLocaleDateString(language); - - return canceled ? ( - Stops on {formattedDate} - ) : ( - Renews on {formattedDate} - ); - }, - }, - { - header: 'Members', - id: 'members', - cell: ({ row }) => { - const memberships = row.original.memberships.filter((item) => !item.code); - const invites = row.original.memberships.length - memberships.length; - const uid = row.original.uuid; - const length = memberships.length; - - return ( - - {length} member{length === 1 ? '' : 's'}{' '} - {invites ? `(${invites} invites)` : ''} - - ); - }, - }, - { - header: '', - id: 'actions', - cell: ({ row }) => { - const organization = row.original; - const uid = organization.uuid; - - return ( -
- - - - - - - Actions - navigator.clipboard.writeText(uid)} - > - Copy UUID - - - - - View Members - - - - - - Delete - - - - -
- ); - }, - }, -]; - -function OrganizationsTable({ - organizations, - pageCount, - perPage, - page, -}: React.PropsWithChildren<{ - organizations: Organizations; - pageCount: number; - perPage: number; - page: number; -}>) { - return ( - - ); -} - -export default OrganizationsTable; diff --git a/apps/web/app/admin/organizations/error.tsx b/apps/web/app/admin/organizations/error.tsx index a0a7ffd74..8f64c6fd4 100644 --- a/apps/web/app/admin/organizations/error.tsx +++ b/apps/web/app/admin/organizations/error.tsx @@ -1,9 +1,9 @@ 'use client'; -import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; - import { PageBody } from '@/components/app/Page'; +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; + function OrganizationsAdminPageError() { return ( diff --git a/apps/web/app/admin/organizations/page.tsx b/apps/web/app/admin/organizations/page.tsx index 12114f54d..d0aea73f3 100644 --- a/apps/web/app/admin/organizations/page.tsx +++ b/apps/web/app/admin/organizations/page.tsx @@ -1,3 +1,5 @@ +import { PageBody } from '@/components/app/Page'; +import appConfig from '@/config/app.config'; import AdminGuard from '@/packages/admin/components/AdminGuard'; import AdminHeader from '@/packages/admin/components/AdminHeader'; @@ -5,10 +7,6 @@ import getSupabaseServerComponentClient from '@packages/supabase/server-componen import { Input } from '@kit/ui/input'; -import appConfig from '@/config/app.config'; - -import { PageBody } from '@/components/app/Page'; - import OrganizationsTable from './components/OrganizationsTable'; import { getOrganizations } from './queries'; diff --git a/apps/web/app/admin/organizations/queries.ts b/apps/web/app/admin/organizations/queries.ts index fce59c3eb..b96f79544 100644 --- a/apps/web/app/admin/organizations/queries.ts +++ b/apps/web/app/admin/organizations/queries.ts @@ -1,7 +1,6 @@ import type { SupabaseClient } from '@supabase/supabase-js'; import type { Database } from '@/database.types'; - import { MEMBERSHIPS_TABLE, ORGANIZATIONS_TABLE } from '@/lib/db-tables'; import type { UserOrganizationData } from '@/lib/organizations/database/queries'; import type MembershipRole from '@/lib/organizations/types/membership-role'; diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx index b8326e830..85c73b695 100644 --- a/apps/web/app/admin/page.tsx +++ b/apps/web/app/admin/page.tsx @@ -1,8 +1,7 @@ -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - +import { PageBody } from '@/components/app/Page'; import appConfig from '@/config/app.config'; -import { PageBody } from '@/components/app/Page'; +import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; import AdminDashboard from '../../packages/admin/components/AdminDashboard'; import AdminGuard from '../../packages/admin/components/AdminGuard'; diff --git a/apps/web/app/admin/users/@modal/[uid]/actions.server.ts b/apps/web/app/admin/users/@modal/[uid]/actions.server.ts index 9f49c25e1..2b98d771c 100644 --- a/apps/web/app/admin/users/@modal/[uid]/actions.server.ts +++ b/apps/web/app/admin/users/@modal/[uid]/actions.server.ts @@ -3,11 +3,11 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { withAdminSession } from '~/admin/lib/actions-utils'; - import { Logger } from '@kit/shared/logger'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; +import { withAdminSession } from '~/admin/lib/actions-utils'; + const getClient = () => getSupabaseServerActionClient({ admin: true }); export const banUser = withAdminSession(async ({ userId }) => { diff --git a/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx b/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx index a6f74014a..676cd9bf5 100644 --- a/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx +++ b/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx @@ -8,6 +8,8 @@ import { useRouter } from 'next/navigation'; import type { User } from '@supabase/gotrue-js'; +import ErrorBoundary from '@/components/app/ErrorBoundary'; + import useCsrfToken from '@kit/hooks/use-csrf-token'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; @@ -20,8 +22,6 @@ import { import { Input } from '@kit/ui/input'; import { Label } from '@kit/ui/label'; -import ErrorBoundary from '@/components/app/ErrorBoundary'; - import { banUser } from '../actions.server'; function BanUserModal({ diff --git a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx b/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx index 420d2b4a9..70cfd8cfc 100644 --- a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx +++ b/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx @@ -4,10 +4,10 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import useSupabase from '@kit/hooks/use-supabase'; - import Spinner from '@/components/app/Spinner'; +import useSupabase from '@kit/hooks/use-supabase'; + function ImpersonateUserAuthSetter({ tokens, }: React.PropsWithChildren<{ diff --git a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx b/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx index 258ed82eb..4a2713ce3 100644 --- a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx +++ b/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx @@ -6,6 +6,9 @@ import { useRouter } from 'next/navigation'; import type { User } from '@supabase/gotrue-js'; +import If from '@/components/app/If'; +import LoadingOverlay from '@/components/app/LoadingOverlay'; + import useCsrfToken from '@kit/hooks/use-csrf-token'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; @@ -16,9 +19,6 @@ import { DialogTitle, } from '@kit/ui/dialog'; -import If from '@/components/app/If'; -import LoadingOverlay from '@/components/app/LoadingOverlay'; - import { impersonateUser } from '../actions.server'; import ImpersonateUserAuthSetter from '../components/ImpersonateUserAuthSetter'; diff --git a/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx b/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx index 7729128df..0db1b50d6 100644 --- a/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx +++ b/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; +import If from '@/components/app/If'; import { EllipsisVerticalIcon } from 'lucide-react'; import { Button } from '@kit/ui/button'; @@ -12,8 +13,6 @@ import { DropdownMenuTrigger, } from '@kit/ui/dropdown-menu'; -import If from '@/components/app/If'; - function UserActionsDropdown({ uid, isBanned, diff --git a/apps/web/app/admin/users/[uid]/page.tsx b/apps/web/app/admin/users/[uid]/page.tsx index 798fa03c5..bb8c44859 100644 --- a/apps/web/app/admin/users/[uid]/page.tsx +++ b/apps/web/app/admin/users/[uid]/page.tsx @@ -1,5 +1,8 @@ import Link from 'next/link'; +import { PageBody } from '@/components/app/Page'; +import configuration from '@/config/app.config'; +import type MembershipRole from '@/lib/organizations/types/membership-role'; import { ChevronRightIcon } from 'lucide-react'; import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; @@ -17,12 +20,6 @@ import { TableRow, } from '@kit/ui/table'; -import configuration from '@/config/app.config'; - -import type MembershipRole from '@/lib/organizations/types/membership-role'; - -import { PageBody } from '@/components/app/Page'; - import RoleBadge from '../../../(app)/[account]/account/organization/components/RoleBadge'; import AdminGuard from '../../../../packages/admin/components/AdminGuard'; import AdminHeader from '../../../../packages/admin/components/AdminHeader'; diff --git a/apps/web/app/admin/users/components/UsersTable.tsx b/apps/web/app/admin/users/components/UsersTable.tsx deleted file mode 100644 index c4957f322..000000000 --- a/apps/web/app/admin/users/components/UsersTable.tsx +++ /dev/null @@ -1,239 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -import type { ColumnDef } from '@tanstack/react-table'; -import { EllipsisIcon } from 'lucide-react'; -import { getI18n } from 'react-i18next'; - -import type UserData from '@kit/session/types/user-data'; -import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar'; -import { Badge } from '@kit/ui/badge'; -import { Button } from '@kit/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@kit/ui/dropdown-menu'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@kit/ui/tooltip'; - -import { DataTable } from '@/components/app/DataTable'; -import If from '@/components/app/If'; - -type UserRow = { - id: string; - email: string | undefined; - phone: string | undefined; - createdAt: string; - updatedAt: string | undefined; - lastSignInAt: string | undefined; - banDuration: string | undefined; - data: UserData; -}; - -const columns: ColumnDef[] = [ - { - header: '', - id: 'avatar', - size: 10, - cell: ({ row }) => { - const user = row.original; - const data = user.data; - const displayName = data?.displayName; - const photoUrl = data?.photoUrl; - const displayText = displayName ?? user.email ?? user.phone ?? ''; - - return ( - - - - {photoUrl ? : null} - {displayText[0]} - - - - {displayText} - - ); - }, - }, - { - header: 'ID', - id: 'id', - size: 30, - cell: ({ row }) => { - const id = row.original.id; - - return ( - - {id} - - ); - }, - }, - { - header: 'Email', - id: 'email', - cell: ({ row }) => { - const email = row.original.email; - - return ( - - {email} - - ); - }, - }, - { - header: 'Name', - size: 50, - id: 'displayName', - cell: ({ row }) => { - return row.original.data?.displayName ?? ''; - }, - }, - { - header: 'Created at', - id: 'createdAt', - cell: ({ row }) => { - const date = new Date(row.original.createdAt); - const i18n = getI18n(); - const language = i18n.language ?? 'en'; - const createdAtLabel = date.toLocaleDateString(language); - - return {createdAtLabel}; - }, - }, - { - header: 'Last sign in', - id: 'lastSignInAt', - cell: ({ row }) => { - const lastSignInAt = row.original.lastSignInAt; - - if (!lastSignInAt) { - return -; - } - - const date = new Date(lastSignInAt); - return {date.toLocaleString()}; - }, - }, - { - header: 'Status', - id: 'status', - cell: ({ row }) => { - const banDuration = row.original.banDuration; - - if (!banDuration || banDuration === 'none') { - return ( - - Active - - ); - } - - return ( - - Banned - - ); - }, - }, - { - header: '', - id: 'actions', - cell: ({ row }) => { - const user = row.original; - const banDuration = row.original.banDuration; - const isBanned = banDuration && banDuration !== 'none'; - - return ( -
- - - - - - Actions - navigator.clipboard.writeText(user.id)} - > - Copy user ID - - - - - - Impersonate User - - - - - - Ban User - - - - - - Delete User - - - - - - - - Reactivate User - - - - - -
- ); - }, - }, -]; - -function UsersTable({ - users, - page, - pageCount, - perPage, -}: React.PropsWithChildren<{ - users: UserRow[]; - pageCount: number; - page: number; - perPage: number; -}>) { - return ( - - ); -} - -export default UsersTable; diff --git a/apps/web/app/admin/users/error.tsx b/apps/web/app/admin/users/error.tsx index 98412d747..5e83f1d7a 100644 --- a/apps/web/app/admin/users/error.tsx +++ b/apps/web/app/admin/users/error.tsx @@ -1,9 +1,9 @@ 'use client'; -import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; - import { PageBody } from '@/components/app/Page'; +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; + function UsersAdminPageError() { return ( diff --git a/apps/web/app/admin/users/page.tsx b/apps/web/app/admin/users/page.tsx index a1507c0a0..ce7401a26 100644 --- a/apps/web/app/admin/users/page.tsx +++ b/apps/web/app/admin/users/page.tsx @@ -1,15 +1,13 @@ +import { PageBody } from '@/components/app/Page'; +import appConfig from '@/config/app.config'; + import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; import type UserData from '@kit/session/types/user-data'; -import appConfig from '@/config/app.config'; - -import { PageBody } from '@/components/app/Page'; - import AdminGuard from '../../../packages/admin/components/AdminGuard'; import AdminHeader from '../../../packages/admin/components/AdminHeader'; import getPageFromQueryParams from '../utils/get-page-from-query-param'; -import UsersTable from './components/UsersTable'; import { getUsers } from './queries'; interface UsersAdminPageProps { @@ -32,14 +30,7 @@ async function UsersAdminPage({ searchParams }: UsersAdminPageProps) {
Users - - - +
); } diff --git a/apps/web/app/admin/users/queries.ts b/apps/web/app/admin/users/queries.ts index c61e6809e..0acb10f24 100644 --- a/apps/web/app/admin/users/queries.ts +++ b/apps/web/app/admin/users/queries.ts @@ -1,7 +1,7 @@ -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - import { USERS_TABLE } from '@/lib/db-tables'; +import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; + export async function getUsers(ids: string[]) { const client = getSupabaseServerComponentClient({ admin: true }); diff --git a/apps/web/app/api/billing/webhook/route.ts b/apps/web/app/api/billing/webhook/route.ts new file mode 100644 index 000000000..00acdb63e --- /dev/null +++ b/apps/web/app/api/billing/webhook/route.ts @@ -0,0 +1,50 @@ +import { getBillingEventHandlerService } from '@kit/billing-gateway'; +import { Logger } from '@kit/shared/logger'; +import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client'; + +import billingConfig from '~/config/billing.config'; + +/** + * @description Handle the webhooks from Stripe related to checkouts + */ +export async function POST(request: Request) { + const client = getSupabaseRouteHandlerClient(); + + // we can infer the provider from the billing config or the request + // for simplicity, we'll use the billing config for now + // TODO: use dynamic provider from request? + const provider = billingConfig.provider; + + Logger.info( + { + name: 'billing', + provider, + }, + `Received billing webhook. Processing...`, + ); + + const service = await getBillingEventHandlerService(client, provider); + + try { + await service.handleWebhookEvent(request); + + Logger.info( + { + name: 'billing', + }, + `Successfully processed billing webhook`, + ); + + return new Response('OK', { status: 200 }); + } catch (e) { + Logger.error( + { + name: 'billing', + error: e, + }, + `Failed to process billing webhook`, + ); + + return new Response('Error', { status: 500 }); + } +} diff --git a/apps/web/app/api/stripe/webhook/route.ts b/apps/web/app/api/stripe/webhook/route.ts deleted file mode 100644 index 8c469487d..000000000 --- a/apps/web/app/api/stripe/webhook/route.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { headers } from 'next/headers'; -import { NextResponse } from 'next/server'; - -import type { SupabaseClient } from '@supabase/supabase-js'; - -import type { Stripe } from 'stripe'; - -import getSupabaseRouteHandlerClient from '@packages/supabase/route-handler-client'; - -import { Logger } from '@kit/logger'; -import createStripeClient from '@kit/stripe/get-stripe'; -import StripeWebhooks from '@kit/stripe/stripe-webhooks.enum'; - -import { setOrganizationSubscriptionData } from '@/lib/organizations/database/mutations'; -import { - addSubscription, - deleteSubscription, - updateSubscriptionById, -} from '@/lib/subscriptions/mutations'; - -const STRIPE_SIGNATURE_HEADER = 'stripe-signature'; - -const webhookSecretKey = process.env.STRIPE_WEBHOOK_SECRET!; - -const logName = 'stripe-webhook'; - -/** - * @description Handle the webhooks from Stripe related to checkouts - */ -export async function POST(request: Request) { - const signature = headers().get(STRIPE_SIGNATURE_HEADER); - - Logger.info(`[Stripe] Received Stripe Webhook`); - - if (!webhookSecretKey) { - Logger.error( - { - name: logName, - }, - `The variable STRIPE_WEBHOOK_SECRET is unset. Please add the STRIPE_WEBHOOK_SECRET environment variable`, - ); - - return new Response(null, { - status: 500, - }); - } - - // verify signature header is not missing - if (!signature) { - return new Response('Invalid signature', { - status: 400, - }); - } - - const rawBody = await request.text(); - const stripe = await createStripeClient(); - - // create an Admin client to write to the subscriptions table - const client = getSupabaseRouteHandlerClient({ - admin: true, - }); - - try { - // build the event from the raw body and signature using Stripe - const event = stripe.webhooks.constructEvent( - rawBody, - signature, - webhookSecretKey, - ); - - Logger.info( - { - name: logName, - type: event.type, - }, - `Processing Stripe Webhook...`, - ); - - switch (event.type) { - case StripeWebhooks.Completed: { - const session = event.data.object as Stripe.Checkout.Session; - const subscriptionId = session.subscription as string; - - const subscription = - await stripe.subscriptions.retrieve(subscriptionId); - - await onCheckoutCompleted(client, session, subscription); - - break; - } - - case StripeWebhooks.SubscriptionDeleted: { - const subscription = event.data.object as Stripe.Subscription; - - await deleteSubscription(client, subscription.id); - - break; - } - - case StripeWebhooks.SubscriptionUpdated: { - const subscription = event.data.object as Stripe.Subscription; - - await updateSubscriptionById(client, subscription); - - break; - } - } - - return NextResponse.json({ success: true }); - } catch (error) { - Logger.error( - { - error, - name: logName, - }, - `Webhook handling failed`, - ); - - return new Response(null, { - status: 500, - }); - } -} - -/** - * @description When the checkout is completed, we store the order. The - * subscription is only activated if the order was paid successfully. - * Otherwise, we have to wait for a further webhook - */ -async function onCheckoutCompleted( - client: SupabaseClient, - session: Stripe.Checkout.Session, - subscription: Stripe.Subscription, -) { - const organizationUid = getOrganizationUidFromClientReference(session); - const customerId = session.customer as string; - - // build organization subscription and set on the organization document - // we add just enough data in the DB, so we do not query - // Stripe for every bit of data - // if you need your DB record to contain further data - // add it to {@link buildOrganizationSubscription} - const { error, data } = await addSubscription(client, subscription); - - if (error) { - return Promise.reject( - `Failed to add subscription to the database: ${error}`, - ); - } - - return setOrganizationSubscriptionData(client, { - organizationUid, - customerId, - subscriptionId: data.id, - }); -} - -/** - * @name getOrganizationUidFromClientReference - * @description Get the organization UUID from the client reference ID - * @param session - */ -function getOrganizationUidFromClientReference( - session: Stripe.Checkout.Session, -) { - return session.client_reference_id!; -} diff --git a/apps/web/app/auth/callback/route.ts b/apps/web/app/auth/callback/route.ts index 741a7397e..b853041d9 100644 --- a/apps/web/app/auth/callback/route.ts +++ b/apps/web/app/auth/callback/route.ts @@ -1,11 +1,11 @@ import { redirect } from 'next/navigation'; import type { NextRequest } from 'next/server'; -import pathsConfig from '~/config/paths.config'; - import { Logger } from '@kit/shared/logger'; import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client'; +import pathsConfig from '~/config/paths.config'; + export async function GET(request: NextRequest) { const requestUrl = new URL(request.url); const searchParams = requestUrl.searchParams; diff --git a/apps/web/app/auth/layout.tsx b/apps/web/app/auth/layout.tsx index 7cc6b0098..ff7b33bc3 100644 --- a/apps/web/app/auth/layout.tsx +++ b/apps/web/app/auth/layout.tsx @@ -1,7 +1,7 @@ -import { AppLogo } from '~/components/app-logo'; - import { AuthLayoutShell } from '@kit/auth/shared'; +import { AppLogo } from '~/components/app-logo'; + function AuthLayout({ children }: React.PropsWithChildren) { return {children}; } diff --git a/apps/web/app/auth/password-reset/page.tsx b/apps/web/app/auth/password-reset/page.tsx index e2e9fe745..b004b2c51 100644 --- a/apps/web/app/auth/password-reset/page.tsx +++ b/apps/web/app/auth/password-reset/page.tsx @@ -1,14 +1,14 @@ import Link from 'next/link'; -import pathsConfig from '~/config/paths.config'; -import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; -import { withI18n } from '~/lib/i18n/with-i18n'; - import { PasswordResetRequestContainer } from '@kit/auth/password-reset'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; +import pathsConfig from '~/config/paths.config'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); diff --git a/apps/web/app/auth/sign-in/page.tsx b/apps/web/app/auth/sign-in/page.tsx index f4aa914eb..d9ca9b2e5 100644 --- a/apps/web/app/auth/sign-in/page.tsx +++ b/apps/web/app/auth/sign-in/page.tsx @@ -1,15 +1,15 @@ import Link from 'next/link'; -import authConfig from '~/config/auth.config'; -import pathsConfig from '~/config/paths.config'; -import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; -import { withI18n } from '~/lib/i18n/with-i18n'; - import { SignInMethodsContainer } from '@kit/auth/sign-in'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; +import authConfig from '~/config/auth.config'; +import pathsConfig from '~/config/paths.config'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); diff --git a/apps/web/app/auth/sign-up/page.tsx b/apps/web/app/auth/sign-up/page.tsx index 8f855d4b7..eab64cfa2 100644 --- a/apps/web/app/auth/sign-up/page.tsx +++ b/apps/web/app/auth/sign-up/page.tsx @@ -1,15 +1,15 @@ import Link from 'next/link'; -import authConfig from '~/config/auth.config'; -import pathsConfig from '~/config/paths.config'; -import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; -import { withI18n } from '~/lib/i18n/with-i18n'; - import { SignUpMethodsContainer } from '@kit/auth/sign-up'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; +import authConfig from '~/config/auth.config'; +import pathsConfig from '~/config/paths.config'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); diff --git a/apps/web/app/auth/verify/page.tsx b/apps/web/app/auth/verify/page.tsx index 350c195c3..bf5c1e308 100644 --- a/apps/web/app/auth/verify/page.tsx +++ b/apps/web/app/auth/verify/page.tsx @@ -1,13 +1,13 @@ import { redirect } from 'next/navigation'; -import pathsConfig from '~/config/paths.config'; -import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; -import { withI18n } from '~/lib/i18n/with-i18n'; - import { MultiFactorChallengeContainer } from '@kit/auth/mfa'; import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import pathsConfig from '~/config/paths.config'; +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; + export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx index e88202f17..b7af07fe5 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/app/error.tsx @@ -3,12 +3,13 @@ import Link from 'next/link'; import { ArrowLeftIcon } from 'lucide-react'; -import { SiteHeader } from '~/(marketing)/components/site-header'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; +import { SiteHeader } from '~/(marketing)/components/site-header'; + const ErrorPage = () => { return (
diff --git a/apps/web/app/join/_components/NewUserInviteForm.tsx b/apps/web/app/join/_components/NewUserInviteForm.tsx index ac2a4ff52..0a4acffbf 100644 --- a/apps/web/app/join/_components/NewUserInviteForm.tsx +++ b/apps/web/app/join/_components/NewUserInviteForm.tsx @@ -2,8 +2,6 @@ import { useCallback, useState, useTransition } from 'react'; -import authConfig from '~/config/auth.config'; - import { EmailOtpContainer } from '@kit/auth/src/components/email-otp-container'; import { OauthProviders } from '@kit/auth/src/components/oauth-providers'; import { PasswordSignInContainer } from '@kit/auth/src/components/password-sign-in-container'; @@ -14,6 +12,8 @@ import { If } from '@kit/ui/if'; import { LoadingOverlay } from '@kit/ui/loading-overlay'; import { Trans } from '@kit/ui/trans'; +import authConfig from '~/config/auth.config'; + enum Mode { SignUp, SignIn, diff --git a/apps/web/app/join/layout.tsx b/apps/web/app/join/layout.tsx index d52af8455..c82ab0cb8 100644 --- a/apps/web/app/join/layout.tsx +++ b/apps/web/app/join/layout.tsx @@ -1,7 +1,7 @@ -import { AppLogo } from '~/components/app-logo'; - import { AuthLayoutShell } from '@kit/auth/shared'; +import { AppLogo } from '~/components/app-logo'; + function InvitePageLayout({ children }: React.PropsWithChildren) { return {children}; } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 50e734bf2..3fa74d4c4 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,13 +1,13 @@ import { Inter as SansFont } from 'next/font/google'; import { cookies } from 'next/headers'; +import { Toaster } from '@kit/ui/sonner'; +import { cn } from '@kit/ui/utils'; + import { RootProviders } from '~/components/root-providers'; import appConfig from '~/config/app.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; -import { Toaster } from '@kit/ui/sonner'; -import { cn } from '@kit/ui/utils'; - import '../styles/globals.css'; const sans = SansFont({ @@ -51,7 +51,7 @@ function getClassName() { export const metadata = { title: appConfig.name, description: appConfig.description, - metadataBase: new URL(appConfig.url!), + metadataBase: new URL(appConfig.url), openGraph: { url: appConfig.url, siteName: appConfig.name, diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index 815170ca0..49f086c1c 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,14 +1,15 @@ import Link from 'next/link'; import { ArrowLeftIcon } from '@radix-ui/react-icons'; -import { SiteHeader } from '~/(marketing)/components/site-header'; -import appConfig from '~/config/app.config'; -import { withI18n } from '~/lib/i18n/with-i18n'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; +import { SiteHeader } from '~/(marketing)/components/site-header'; +import appConfig from '~/config/app.config'; +import { withI18n } from '~/lib/i18n/with-i18n'; + export const metadata = { title: `Page not found - ${appConfig.name}`, }; diff --git a/apps/web/app/server-sitemap.xml/route.ts b/apps/web/app/server-sitemap.xml/route.ts index 5aadb52a8..2e865552d 100644 --- a/apps/web/app/server-sitemap.xml/route.ts +++ b/apps/web/app/server-sitemap.xml/route.ts @@ -1,10 +1,9 @@ +import appConfig from '@/config/app.config'; import { invariant } from '@epic-web/invariant'; import { allDocumentationPages, allPosts } from 'contentlayer/generated'; import { getServerSideSitemap } from 'next-sitemap'; import { join } from 'path'; -import appConfig from '@/config/app.config'; - const siteUrl = appConfig.url; export async function GET() { diff --git a/apps/web/app/update-password/page.tsx b/apps/web/app/update-password/page.tsx index 88b38412b..95566fa19 100644 --- a/apps/web/app/update-password/page.tsx +++ b/apps/web/app/update-password/page.tsx @@ -1,12 +1,12 @@ import { redirect } from 'next/navigation'; -import pathsConfig from '~/config/paths.config'; -import { withI18n } from '~/lib/i18n/with-i18n'; - import { AuthLayoutShell } from '@kit/auth/shared'; import PasswordResetForm from '@kit/auth/src/components/password-reset-form'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import pathsConfig from '~/config/paths.config'; +import { withI18n } from '~/lib/i18n/with-i18n'; + async function PasswordResetPage() { const client = getSupabaseServerComponentClient(); const user = await client.auth.getUser(); diff --git a/apps/web/components/root-providers.tsx b/apps/web/components/root-providers.tsx index 1414f94a6..2b5d29234 100644 --- a/apps/web/components/root-providers.tsx +++ b/apps/web/components/root-providers.tsx @@ -1,12 +1,13 @@ 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import pathsConfig from '~/config/paths.config'; -import { i18nResolver } from '~/lib/i18n/i18n.resolver'; import { I18nProvider } from '@kit/i18n/provider'; import { AuthChangeListener } from '@kit/supabase/components/auth-change-listener'; +import pathsConfig from '~/config/paths.config'; +import { i18nResolver } from '~/lib/i18n/i18n.resolver'; + export function RootProviders({ lang, children, diff --git a/apps/web/config/organization-account-sidebar.config.tsx b/apps/web/config/organization-account-sidebar.config.tsx index d34997b67..3dee549af 100644 --- a/apps/web/config/organization-account-sidebar.config.tsx +++ b/apps/web/config/organization-account-sidebar.config.tsx @@ -4,11 +4,12 @@ import { SettingsIcon, UsersIcon, } from 'lucide-react'; -import featureFlagsConfig from '~/config/feature-flags.config'; -import pathsConfig from '~/config/paths.config'; import { SidebarConfigSchema } from '@kit/ui/sidebar-schema'; +import featureFlagsConfig from '~/config/feature-flags.config'; +import pathsConfig from '~/config/paths.config'; + const iconClasses = 'w-4'; const routes = (account: string) => [ diff --git a/apps/web/config/personal-account-sidebar.config.tsx b/apps/web/config/personal-account-sidebar.config.tsx index 0e8042145..00d749763 100644 --- a/apps/web/config/personal-account-sidebar.config.tsx +++ b/apps/web/config/personal-account-sidebar.config.tsx @@ -1,9 +1,10 @@ import { CreditCardIcon, HomeIcon, UserIcon } from 'lucide-react'; -import featureFlagsConfig from '~/config/feature-flags.config'; -import pathsConfig from '~/config/paths.config'; import { SidebarConfigSchema } from '@kit/ui/sidebar-schema'; +import featureFlagsConfig from '~/config/feature-flags.config'; +import pathsConfig from '~/config/paths.config'; + const iconClasses = 'w-4'; const routes = [ diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index d07d32972..1a5439c30 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -2,12 +2,13 @@ import type { NextRequest } from 'next/server'; import { NextResponse, URLPattern } from 'next/server'; import csrf from 'edge-csrf'; -import appConfig from '~/config/app.config'; -import pathsConfig from '~/config/paths.config'; import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa'; import { createMiddlewareClient } from '@kit/supabase/middleware-client'; +import appConfig from '~/config/app.config'; +import pathsConfig from '~/config/paths.config'; + const CSRF_SECRET_COOKIE = 'csrfSecret'; const NEXT_ACTION_HEADER = 'next-action'; diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 96574a5a4..2484cdfd1 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,3 +1,5 @@ +import withBundleAnalyzer from '@next/bundle-analyzer'; + /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, @@ -12,16 +14,17 @@ const config = { '@kit/i18n', '@kit/mailers', '@kit/billing', - '@kit/billing-gateway' + '@kit/billing-gateway', ], - pageExtensions: ['ts', 'tsx', 'mdx'], + pageExtensions: ['ts', 'tsx'], experimental: { mdxRs: true, }, - /** We already do linting and typechecking as separate tasks in CI */ eslint: { ignoreDuringBuilds: true }, typescript: { ignoreBuildErrors: true }, }; -export default config; +export default withBundleAnalyzer({ + enabled: process.env.ANALYZE === 'true', +})(config); diff --git a/apps/web/package.json b/apps/web/package.json index 6504560ab..f0b28e9bd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "analyze": "ANALYZE=true pnpm run build", "build": "pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", "dev": "pnpm with-env next dev", @@ -57,6 +58,7 @@ "@kit/prettier-config": "^0.1.0", "@kit/tailwind-config": "^0.1.0", "@kit/tsconfig": "^0.1.0", + "@next/bundle-analyzer": "^14.1.4", "@types/mdx": "^2.0.10", "@types/node": "^20.11.5", "@types/react": "^18.2.48", diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index 3089ddfe4..92ef71bc2 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -56,5 +56,9 @@ "label": "Member", "description": "Cannot invite members or change settings" } + }, + "billingInterval": { + "month": "Billed monthly", + "year": "Billed yearly" } } diff --git a/packages/billing-gateway/package.json b/packages/billing-gateway/package.json index 67a0ff70b..6d3bafc15 100644 --- a/packages/billing-gateway/package.json +++ b/packages/billing-gateway/package.json @@ -16,20 +16,20 @@ "peerDependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", - "zod": "^3.22.4" - }, - "dependencies": { + "zod": "^3.22.4", "@kit/ui": "0.1.0", "@kit/stripe": "0.1.0", "@kit/billing": "0.1.0", "@kit/supabase": "^0.1.0", + "@kit/shared": "^0.1.0", "lucide-react": "^0.361.0" }, "devDependencies": { "@kit/prettier-config": "0.1.0", "@kit/eslint-config": "0.2.0", "@kit/tailwind-config": "0.1.0", - "@kit/tsconfig": "0.1.0" + "@kit/tsconfig": "0.1.0", + "@supabase/supabase-js": "^2.39.8" }, "eslintConfig": { "root": true, diff --git a/packages/billing-gateway/src/components/plan-picker.tsx b/packages/billing-gateway/src/components/plan-picker.tsx index 463ffb895..c72a8bee8 100644 --- a/packages/billing-gateway/src/components/plan-picker.tsx +++ b/packages/billing-gateway/src/components/plan-picker.tsx @@ -22,6 +22,7 @@ import { RadioGroupItemLabel, } from '@kit/ui/radio-group'; import { Trans } from '@kit/ui/trans'; +import { cn } from '@kit/ui/utils'; export function PlanPicker( props: React.PropsWithChildren<{ @@ -81,29 +82,39 @@ export function PlanPicker( - {intervals.map((interval) => { - return ( -
- { - form.setValue('interval', interval); - }} - /> +
+ {intervals.map((interval) => { + const selected = field.value === interval; - - + { + form.setValue('planId', ''); + form.setValue('interval', interval); + }} /> - -
- ); - })} + + + + + + ); + })} +
@@ -130,7 +141,10 @@ export function PlanPicker( } return ( - +