Remove billing and checkout redirect buttons and related services
Deleted the billing-redirect-button, checkout-redirect-button, and embedded-stripe-checkout components. Additionally, removed the shadcn directory, which encompassed billing-related icons. This change streamlines the subscription settings interface and organizes the system's payment management. This update is a stepping stone towards improving the billing system's overall architecture.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
<FormControl>
|
||||
<RadioGroup name={field.name} value={field.value}>
|
||||
{intervals.map((interval) => {
|
||||
return (
|
||||
<div
|
||||
key={interval}
|
||||
className={'flex items-center space-x-2'}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={interval}
|
||||
value={interval}
|
||||
onClick={() => {
|
||||
form.setValue('interval', interval);
|
||||
}}
|
||||
/>
|
||||
<div className={'flex space-x-2.5'}>
|
||||
{intervals.map((interval) => {
|
||||
const selected = field.value === interval;
|
||||
|
||||
<span className={'text-sm font-bold'}>
|
||||
<Trans
|
||||
i18nKey={`common.billingInterval.${interval}`}
|
||||
defaults={interval}
|
||||
return (
|
||||
<label
|
||||
key={interval}
|
||||
className={cn(
|
||||
'hover:bg-muted flex items-center space-x-2 rounded-md border border-transparent px-4 py-2',
|
||||
{
|
||||
['border-border']: selected,
|
||||
['hover:bg-muted']: !selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={interval}
|
||||
value={interval}
|
||||
onClick={() => {
|
||||
form.setValue('planId', '');
|
||||
form.setValue('interval', interval);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<span className={'text-sm font-bold'}>
|
||||
<Trans
|
||||
i18nKey={`common:billingInterval.${interval}`}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -130,7 +141,10 @@ export function PlanPicker(
|
||||
}
|
||||
|
||||
return (
|
||||
<RadioGroupItemLabel key={variant.id}>
|
||||
<RadioGroupItemLabel
|
||||
selected={field.value === variant.id}
|
||||
key={variant.id}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={variant.id}
|
||||
value={variant.id}
|
||||
@@ -144,9 +158,7 @@ export function PlanPicker(
|
||||
>
|
||||
<Label
|
||||
htmlFor={variant.id}
|
||||
className={
|
||||
'flex flex-col justify-center space-y-1.5'
|
||||
}
|
||||
className={'flex flex-col justify-center space-y-2'}
|
||||
>
|
||||
<span className="font-bold">{item.name}</span>
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './billing-gateway-service';
|
||||
export * from './gateway-provider-factory';
|
||||
export * from './services/billing-gateway/billing-gateway.service';
|
||||
export * from './services/billing-gateway/billing-gateway-provider-factory';
|
||||
export * from './services/billing-event-handler/billing-gateway-provider-factory';
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { BillingWebhookHandlerService } from '@kit/billing';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class BillingEventHandlerService {
|
||||
constructor(
|
||||
private readonly client: SupabaseClient<Database>,
|
||||
private readonly strategy: BillingWebhookHandlerService,
|
||||
) {}
|
||||
|
||||
async handleWebhookEvent(request: Request) {
|
||||
const event = await this.strategy.verifyWebhookSignature(request);
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
|
||||
return this.strategy.handleWebhookEvent(event, {
|
||||
onSubscriptionDeleted: async (subscriptionId: string) => {
|
||||
// Handle the subscription deleted event
|
||||
// here we delete the subscription from the database
|
||||
Logger.info(
|
||||
{
|
||||
namespace: 'billing',
|
||||
subscriptionId,
|
||||
},
|
||||
'Processing subscription deleted event',
|
||||
);
|
||||
|
||||
const { error } = await this.client
|
||||
.from('subscriptions')
|
||||
.delete()
|
||||
.match({ id: subscriptionId });
|
||||
|
||||
if (error) {
|
||||
throw new Error('Failed to delete subscription');
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
namespace: 'billing',
|
||||
subscriptionId,
|
||||
},
|
||||
'Successfully deleted subscription',
|
||||
);
|
||||
},
|
||||
onSubscriptionUpdated: async (subscription) => {
|
||||
const ctx = {
|
||||
namespace: 'billing',
|
||||
subscriptionId: subscription.id,
|
||||
provider: subscription.billing_provider,
|
||||
accountId: subscription.account_id,
|
||||
};
|
||||
|
||||
Logger.info(ctx, 'Processing subscription updated event');
|
||||
|
||||
// Handle the subscription updated event
|
||||
// here we update the subscription in the database
|
||||
const { error } = await this.client
|
||||
.from('subscriptions')
|
||||
.update(subscription)
|
||||
.match({ id: subscription.id });
|
||||
|
||||
if (error) {
|
||||
Logger.error(
|
||||
{
|
||||
error,
|
||||
...ctx,
|
||||
},
|
||||
'Failed to update subscription',
|
||||
);
|
||||
|
||||
throw new Error('Failed to update subscription');
|
||||
}
|
||||
|
||||
Logger.info(ctx, 'Successfully updated subscription');
|
||||
},
|
||||
onCheckoutSessionCompleted: async (subscription) => {
|
||||
// Handle the checkout session completed event
|
||||
// here we add the subscription to the database
|
||||
const ctx = {
|
||||
namespace: 'billing',
|
||||
subscriptionId: subscription.id,
|
||||
provider: subscription.billing_provider,
|
||||
accountId: subscription.account_id,
|
||||
};
|
||||
|
||||
Logger.info(ctx, 'Processing checkout session completed event...');
|
||||
|
||||
const { error } = await this.client.rpc('add_subscription', {
|
||||
subscription,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
Logger.error(ctx, 'Failed to add subscription');
|
||||
|
||||
throw new Error('Failed to add subscription');
|
||||
}
|
||||
|
||||
Logger.info(ctx, 'Successfully added subscription');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BillingProvider, BillingWebhookHandlerService } from '@kit/billing';
|
||||
|
||||
export class BillingEventHandlerFactoryService {
|
||||
static async GetProviderStrategy(
|
||||
provider: z.infer<typeof BillingProvider>,
|
||||
): Promise<BillingWebhookHandlerService> {
|
||||
switch (provider) {
|
||||
case 'stripe': {
|
||||
const { StripeWebhookHandlerService } = await import('@kit/stripe');
|
||||
|
||||
return new StripeWebhookHandlerService();
|
||||
}
|
||||
|
||||
case 'paddle': {
|
||||
throw new Error('Paddle is not supported yet');
|
||||
}
|
||||
|
||||
case 'lemon-squeezy': {
|
||||
throw new Error('Lemon Squeezy is not supported yet');
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported billing provider: ${provider as string}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { BillingEventHandlerService } from './billing-event-handler.service';
|
||||
import { BillingEventHandlerFactoryService } from './billing-gateway-factory.service';
|
||||
|
||||
/**
|
||||
* @description This function retrieves the billing provider from the database and returns a
|
||||
* new instance of the `BillingGatewayService` class. This class is used to interact with the server actions
|
||||
* defined in the host application.
|
||||
*/
|
||||
export async function getBillingEventHandlerService(
|
||||
client: ReturnType<typeof getSupabaseServerActionClient>,
|
||||
provider: Database['public']['Enums']['billing_provider'],
|
||||
) {
|
||||
const strategy =
|
||||
await BillingEventHandlerFactoryService.GetProviderStrategy(provider);
|
||||
|
||||
return new BillingEventHandlerService(client, strategy);
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { BillingGatewayService } from './billing-gateway-service';
|
||||
import { BillingGatewayService } from './billing-gateway.service';
|
||||
|
||||
/**
|
||||
* @description This function retrieves the billing provider from the database and returns a
|
||||
* new instance of the `BillingGatewayService` class. This class is used to interact with the server actions
|
||||
* defined in the host application.
|
||||
* @param {ReturnType<typeof getSupabaseServerActionClient>} client - The Supabase server action client.
|
||||
*
|
||||
*/
|
||||
export async function getGatewayProvider(
|
||||
export async function getBillingGatewayProvider(
|
||||
client: ReturnType<typeof getSupabaseServerActionClient>,
|
||||
) {
|
||||
const provider = await getBillingProvider(client);
|
||||
@@ -17,10 +17,9 @@
|
||||
"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/supabase": "0.1.0",
|
||||
"lucide-react": "^0.361.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './create-billing-schema';
|
||||
export * from './services/billing-strategy-provider.service';
|
||||
export * from './services/billing-webhook-handler.service';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
type SubscriptionObject = Database['public']['Tables']['subscriptions'];
|
||||
|
||||
type SubscriptionInsertParams = Omit<
|
||||
SubscriptionObject['Insert'],
|
||||
'billing_customer_id'
|
||||
>;
|
||||
|
||||
type SubscriptionUpdateParams = SubscriptionObject['Update'];
|
||||
|
||||
/**
|
||||
* Represents an abstract class for handling billing webhook events.
|
||||
*/
|
||||
export abstract class BillingWebhookHandlerService {
|
||||
abstract verifyWebhookSignature(request: Request): Promise<unknown>;
|
||||
|
||||
abstract handleWebhookEvent(
|
||||
event: unknown,
|
||||
params: {
|
||||
onCheckoutSessionCompleted: (
|
||||
subscription: SubscriptionInsertParams,
|
||||
) => Promise<unknown>;
|
||||
|
||||
onSubscriptionUpdated: (
|
||||
subscription: SubscriptionUpdateParams,
|
||||
) => Promise<unknown>;
|
||||
|
||||
onSubscriptionDeleted: (subscriptionId: string) => Promise<unknown>;
|
||||
},
|
||||
): Promise<unknown>;
|
||||
}
|
||||
@@ -14,13 +14,6 @@
|
||||
"./personal-account-settings": "./src/components/personal-account-settings/index.ts",
|
||||
"./hooks/*": "./src/hooks/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kit/supabase": "0.1.0",
|
||||
"@kit/ui": "0.1.0",
|
||||
"@kit/shared": "0.1.0",
|
||||
"lucide-react": "^0.360.0",
|
||||
"@radix-ui/react-icons": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "0.2.0",
|
||||
"@kit/prettier-config": "0.1.0",
|
||||
@@ -29,7 +22,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"@kit/supabase": "0.1.0",
|
||||
"@kit/ui": "0.1.0",
|
||||
"@kit/shared": "0.1.0",
|
||||
"lucide-react": "^0.360.0",
|
||||
"@radix-ui/react-icons": "^1.3.0"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { PageHeader } from '@/components/app/Page';
|
||||
import pathsConfig from '@/config/paths.config';
|
||||
import { ArrowLeftIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
|
||||
import pathsConfig from '@/config/paths.config';
|
||||
|
||||
import { PageHeader } from '@/components/app/Page';
|
||||
|
||||
function AdminHeader({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<PageHeader
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { HomeIcon, UserIcon, UsersIcon } from 'lucide-react';
|
||||
|
||||
import Logo from '@/components/app/Logo';
|
||||
import { Sidebar, SidebarContent, SidebarItem } from '@/components/app/Sidebar';
|
||||
import { HomeIcon, UserIcon, UsersIcon } from 'lucide-react';
|
||||
|
||||
function AdminSidebar() {
|
||||
return (
|
||||
|
||||
@@ -15,19 +15,18 @@
|
||||
"./shared": "./src/shared.ts",
|
||||
"./mfa": "./src/mfa.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kit/ui": "0.1.0",
|
||||
"@kit/supabase": "0.1.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"react-i18next": "14.1.0",
|
||||
"sonner": "^1.4.41",
|
||||
"@tanstack/react-query": "5.28.6"
|
||||
},
|
||||
"dependencies": {},
|
||||
"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",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@kit/ui": "0.1.0",
|
||||
"@kit/supabase": "0.1.0",
|
||||
"react-i18next": "14.1.0",
|
||||
"sonner": "^1.4.41",
|
||||
"@tanstack/react-query": "5.28.6"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
"./cookie": "./src/get-language-cookie.ts",
|
||||
"./provider": "./src/I18nProvider.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kit/shared": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/prettier-config": "0.1.0",
|
||||
"@kit/eslint-config": "0.2.0",
|
||||
@@ -29,7 +26,8 @@
|
||||
"react-i18next": "^14.1.0",
|
||||
"i18next-browser-languagedetector": "7.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"next": "^14.1.4"
|
||||
"next": "^14.1.4",
|
||||
"@kit/shared": "^0.1.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
@@ -35,7 +35,6 @@ async function withI18nClient(lang: string, resolver: Resolver) {
|
||||
}
|
||||
|
||||
async function loadClientI18n(lang: string | undefined, resolver: Resolver) {
|
||||
// TODO: pull cookie client-side
|
||||
const { initializeI18nClient } = await import('./i18n.client');
|
||||
|
||||
return initializeI18nClient(lang, resolver);
|
||||
|
||||
@@ -23,7 +23,7 @@ export function initializeI18nClient(
|
||||
.use(
|
||||
resourcesToBackend(async (language, namespace, callback) => {
|
||||
const data = await i18nResolver(language, namespace);
|
||||
|
||||
console.log(data);
|
||||
return callback(null, data);
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
".": "./src/index.ts",
|
||||
"./components": "./src/components/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@kit/billing": "0.1.0",
|
||||
"@kit/ui": "0.1.0",
|
||||
"@kit/shared": "0.1.0",
|
||||
"@kit/supabase": "0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"stripe": "^14.21.0",
|
||||
"@stripe/react-stripe-js": "^2.6.2",
|
||||
"@stripe/stripe-js": "^3.0.10",
|
||||
"@kit/billing": "0.1.0",
|
||||
"@kit/ui": "0.1.0"
|
||||
"@stripe/stripe-js": "^3.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/prettier-config": "0.1.0",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { StripeBillingStrategyService } from './stripe.service';
|
||||
export { StripeBillingStrategyService } from './services/stripe-billing-strategy.service';
|
||||
export { StripeWebhookHandlerService } from './services/stripe-webhook-handler.service';
|
||||
|
||||
5
packages/stripe/src/schema/stripe-client-env.schema.ts
Normal file
5
packages/stripe/src/schema/stripe-client-env.schema.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const StripeClientEnvSchema = z.object({
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().min(1),
|
||||
});
|
||||
6
packages/stripe/src/schema/stripe-server-env.schema.ts
Normal file
6
packages/stripe/src/schema/stripe-server-env.schema.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const StripeServerEnvSchema = z.object({
|
||||
STRIPE_SECRET_KEY: z.string().min(1),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().min(1),
|
||||
});
|
||||
@@ -1,328 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { RedirectType } from 'next/dist/client/components/redirect';
|
||||
import { headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { join } from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import getSupabaseServerActionClient from '@packages/supabase/action-client';
|
||||
|
||||
import { withSession } from '@kit/generic/actions-utils';
|
||||
import getLogger from '@kit/logger';
|
||||
|
||||
import pathsConfig from '@/config/paths.config';
|
||||
import pricingConfig from '@/config/pricing.config';
|
||||
|
||||
import { getUserMembershipByOrganization } from '@/lib/memberships/queries';
|
||||
import {
|
||||
getOrganizationByCustomerId,
|
||||
getOrganizationByUid,
|
||||
} from '@/lib/organizations/database/queries';
|
||||
import { canChangeBilling } from '@/lib/organizations/permissions';
|
||||
import requireSession from '@/lib/user/require-session';
|
||||
|
||||
import { stripe } from './stripe.service';
|
||||
|
||||
export const createCheckoutAction = withSession(
|
||||
async (_, formData: FormData) => {
|
||||
const logger = getLogger();
|
||||
|
||||
const bodyResult = await getCheckoutBodySchema().safeParseAsync(
|
||||
Object.fromEntries(formData),
|
||||
);
|
||||
|
||||
const redirectToErrorPage = (error?: string) => {
|
||||
const referer = headers().get('referer')!;
|
||||
const url = join(referer, `?error=true`);
|
||||
|
||||
logger.error({ error }, `Could not create Stripe Checkout session`);
|
||||
|
||||
return redirect(url);
|
||||
};
|
||||
|
||||
// Validate the body schema
|
||||
if (!bodyResult.success) {
|
||||
return redirectToErrorPage(`Invalid request body`);
|
||||
}
|
||||
|
||||
const { organizationUid, priceId, returnUrl } = bodyResult.data;
|
||||
|
||||
// create the Supabase client
|
||||
const client = getSupabaseServerActionClient();
|
||||
|
||||
// require the user to be logged in
|
||||
const sessionResult = await requireSession(client);
|
||||
const userId = sessionResult.user.id;
|
||||
const customerEmail = sessionResult.user.email;
|
||||
|
||||
const { error, data } = await getOrganizationByUid(client, organizationUid);
|
||||
|
||||
if (error) {
|
||||
return redirectToErrorPage(`Organization not found`);
|
||||
}
|
||||
|
||||
const customerId = data?.subscription?.customerId;
|
||||
|
||||
if (customerId) {
|
||||
logger.info({ customerId }, `Customer ID found for organization`);
|
||||
}
|
||||
|
||||
const plan = getPlanByPriceId(priceId);
|
||||
|
||||
// check if the plan exists in the appConfig.
|
||||
if (!plan) {
|
||||
console.warn(
|
||||
`Plan not found for price ID "${priceId}". Did you forget to add it to the configuration? If the Price ID is incorrect, the checkout will be rejected. Please check the Stripe dashboard`,
|
||||
);
|
||||
}
|
||||
|
||||
// check the user's role has access to the checkout
|
||||
const canChangeBilling = await getUserCanAccessCheckout(client, {
|
||||
organizationUid,
|
||||
userId,
|
||||
});
|
||||
|
||||
// disallow if the user doesn't have permissions to change
|
||||
// billing account based on its role. To change the logic, please update
|
||||
// {@link canChangeBilling}
|
||||
if (!canChangeBilling) {
|
||||
logger.debug(
|
||||
{
|
||||
userId,
|
||||
organizationUid,
|
||||
},
|
||||
`User attempted to access checkout but lacked permissions`,
|
||||
);
|
||||
|
||||
return redirectToErrorPage(
|
||||
`You do not have permission to access this page`,
|
||||
);
|
||||
}
|
||||
|
||||
const trialPeriodDays =
|
||||
plan && 'trialPeriodDays' in plan
|
||||
? (plan.trialPeriodDays as number)
|
||||
: undefined;
|
||||
|
||||
// create the Stripe Checkout session
|
||||
const session = await stripe
|
||||
.createCheckout({
|
||||
returnUrl,
|
||||
organizationUid,
|
||||
priceId,
|
||||
customerId,
|
||||
trialPeriodDays,
|
||||
customerEmail,
|
||||
embedded: true,
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(e, `Stripe Checkout error`);
|
||||
});
|
||||
|
||||
// if there was an error, redirect to the error page
|
||||
if (!session) {
|
||||
return redirectToErrorPage();
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
id: session.id,
|
||||
organizationUid,
|
||||
},
|
||||
`Created Stripe Checkout session`,
|
||||
);
|
||||
|
||||
if (!session.client_secret) {
|
||||
logger.error(
|
||||
{ id: session.id },
|
||||
`Stripe Checkout session missing client secret`,
|
||||
);
|
||||
|
||||
return redirectToErrorPage();
|
||||
}
|
||||
|
||||
// if the checkout is embedded, we need to render the checkout
|
||||
// therefore, we send the clientSecret back to the client
|
||||
logger.info(
|
||||
{ id: session.id },
|
||||
`Using embedded checkout mode. Sending client secret back to client.`,
|
||||
);
|
||||
|
||||
return {
|
||||
clientSecret: session.client_secret,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name getUserCanAccessCheckout
|
||||
* @description check if the user has permissions to access the checkout
|
||||
* @param client
|
||||
* @param params
|
||||
*/
|
||||
async function getUserCanAccessCheckout(
|
||||
client: SupabaseClient,
|
||||
params: {
|
||||
organizationUid: string;
|
||||
userId: string;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const { role } = await getUserMembershipByOrganization(client, params);
|
||||
|
||||
if (role === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return canChangeBilling(role);
|
||||
} catch (error) {
|
||||
getLogger().error({ error }, `Could not retrieve user role`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const createBillingPortalSessionAction = withSession(
|
||||
async (formData: FormData) => {
|
||||
const body = Object.fromEntries(formData);
|
||||
const bodyResult = await getBillingPortalBodySchema().safeParseAsync(body);
|
||||
const referrerPath = getReferrer();
|
||||
|
||||
// Validate the body schema
|
||||
if (!bodyResult.success) {
|
||||
return redirectToErrorPage(referrerPath);
|
||||
}
|
||||
|
||||
const { customerId } = bodyResult.data;
|
||||
|
||||
const client = getSupabaseServerActionClient();
|
||||
const logger = getLogger();
|
||||
const session = await requireSession(client);
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
// get permissions to see if the user can access the portal
|
||||
const canAccess = await getUserCanAccessCustomerPortal(client, {
|
||||
customerId,
|
||||
userId,
|
||||
});
|
||||
|
||||
// validate that the user can access the portal
|
||||
if (!canAccess) {
|
||||
return redirectToErrorPage(referrerPath);
|
||||
}
|
||||
|
||||
const referer = headers().get('referer');
|
||||
const origin = headers().get('origin');
|
||||
const returnUrl = referer || origin || pathsConfig.appHome;
|
||||
|
||||
// get the Stripe Billing Portal session
|
||||
const { url } = await stripe
|
||||
.createBillingPortalSession({
|
||||
returnUrl,
|
||||
customerId,
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(e, `Stripe Billing Portal redirect error`);
|
||||
|
||||
return redirectToErrorPage(referrerPath);
|
||||
});
|
||||
|
||||
// redirect to the Stripe Billing Portal
|
||||
return redirect(url, RedirectType.replace);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name getUserCanAccessCustomerPortal
|
||||
* @description Returns whether a user {@link userId} has access to the
|
||||
* Stripe portal of an organization with customer ID {@link customerId}
|
||||
*/
|
||||
async function getUserCanAccessCustomerPortal(
|
||||
client: SupabaseClient,
|
||||
params: {
|
||||
customerId: string;
|
||||
userId: string;
|
||||
},
|
||||
) {
|
||||
const logger = getLogger();
|
||||
|
||||
const { data: organization, error } = await getOrganizationByCustomerId(
|
||||
client,
|
||||
params.customerId,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
{
|
||||
error,
|
||||
customerId: params.customerId,
|
||||
},
|
||||
`Could not retrieve organization by Customer ID`,
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const organizationUid = organization.uuid;
|
||||
|
||||
const { role } = await getUserMembershipByOrganization(client, {
|
||||
organizationUid,
|
||||
userId: params.userId,
|
||||
});
|
||||
|
||||
if (role === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return canChangeBilling(role);
|
||||
} catch (error) {
|
||||
logger.error({ error }, `Could not retrieve user role`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getBillingPortalBodySchema() {
|
||||
return z.object({
|
||||
customerId: z.string().min(1),
|
||||
});
|
||||
}
|
||||
|
||||
function getCheckoutBodySchema() {
|
||||
return z.object({
|
||||
organizationUid: z.string().uuid(),
|
||||
priceId: z.string().min(1),
|
||||
returnUrl: z.string().min(1),
|
||||
});
|
||||
}
|
||||
|
||||
function getPlanByPriceId(priceId: string) {
|
||||
const products = pricingConfig.products;
|
||||
type Plan = (typeof products)[0]['plans'][0];
|
||||
|
||||
return products.reduce<Maybe<Plan>>((acc, product) => {
|
||||
if (acc) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return product.plans.find(({ stripePriceId }) => stripePriceId === priceId);
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
function redirectToErrorPage(referrerPath: string) {
|
||||
const url = join(referrerPath, `?error=true`);
|
||||
|
||||
return redirect(url);
|
||||
}
|
||||
|
||||
function getReferrer() {
|
||||
const referer = headers().get('referer');
|
||||
const origin = headers().get('origin');
|
||||
return referer || origin || pathsConfig.appHome;
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
RetrieveCheckoutSessionSchema,
|
||||
} from '@kit/billing/schema';
|
||||
|
||||
import { createStripeCheckout } from './create-checkout';
|
||||
import { createStripeBillingPortalSession } from './create-stripe-billing-portal-session';
|
||||
import { createStripeCheckout } from './create-stripe-checkout';
|
||||
import { createStripeClient } from './stripe-sdk';
|
||||
|
||||
export class StripeBillingStrategyService
|
||||
@@ -22,7 +22,13 @@ export class StripeBillingStrategyService
|
||||
) {
|
||||
const stripe = await this.stripeProvider();
|
||||
|
||||
return createStripeCheckout(stripe, params);
|
||||
const { client_secret } = await createStripeCheckout(stripe, params);
|
||||
|
||||
if (!client_secret) {
|
||||
throw new Error('Failed to create checkout session');
|
||||
}
|
||||
|
||||
return { checkoutToken: client_secret };
|
||||
}
|
||||
|
||||
async createBillingPortalSession(
|
||||
23
packages/stripe/src/services/stripe-sdk.ts
Normal file
23
packages/stripe/src/services/stripe-sdk.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'server-only';
|
||||
|
||||
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
|
||||
|
||||
const STRIPE_API_VERSION = '2023-10-16';
|
||||
|
||||
// Parse the environment variables and validate them
|
||||
const stripeServerEnv = StripeServerEnvSchema.parse({
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns a Stripe instance
|
||||
*/
|
||||
export async function createStripeClient() {
|
||||
const { default: Stripe } = await import('stripe');
|
||||
const key = stripeServerEnv.STRIPE_SECRET_KEY;
|
||||
|
||||
return new Stripe(key, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
}
|
||||
208
packages/stripe/src/services/stripe-webhook-handler.service.ts
Normal file
208
packages/stripe/src/services/stripe-webhook-handler.service.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingWebhookHandlerService } from '@kit/billing';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
|
||||
import { createStripeClient } from './stripe-sdk';
|
||||
|
||||
type Subscription = Database['public']['Tables']['subscriptions'];
|
||||
|
||||
type InsertSubscriptionParams = Omit<
|
||||
Subscription['Insert'],
|
||||
'billing_customer_id'
|
||||
>;
|
||||
|
||||
export class StripeWebhookHandlerService
|
||||
implements BillingWebhookHandlerService
|
||||
{
|
||||
private stripe: Stripe | undefined;
|
||||
|
||||
private readonly provider: Database['public']['Enums']['billing_provider'] =
|
||||
'stripe';
|
||||
|
||||
private readonly namespace = 'billing.stripe';
|
||||
|
||||
/**
|
||||
* @description Verifies the webhook signature - should throw an error if the signature is invalid
|
||||
*/
|
||||
async verifyWebhookSignature(request: Request) {
|
||||
const body = await request.clone().text();
|
||||
const signature = `stripe-signature`;
|
||||
|
||||
const { STRIPE_WEBHOOK_SECRET } = StripeServerEnvSchema.parse({
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
const stripe = await this.loadStripe();
|
||||
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
private async loadStripe() {
|
||||
if (!this.stripe) {
|
||||
this.stripe = await createStripeClient();
|
||||
}
|
||||
|
||||
return this.stripe;
|
||||
}
|
||||
|
||||
async handleWebhookEvent(
|
||||
event: Stripe.Event,
|
||||
params: {
|
||||
onCheckoutSessionCompleted: (
|
||||
data: InsertSubscriptionParams,
|
||||
customerId: string,
|
||||
) => Promise<unknown>;
|
||||
|
||||
onSubscriptionUpdated: (data: Subscription['Update']) => Promise<unknown>;
|
||||
onSubscriptionDeleted: (subscriptionId: string) => Promise<unknown>;
|
||||
},
|
||||
) {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed': {
|
||||
return this.handleCheckoutSessionCompleted(
|
||||
event,
|
||||
params.onCheckoutSessionCompleted,
|
||||
);
|
||||
}
|
||||
|
||||
case 'customer.subscription.updated': {
|
||||
return this.handleSubscriptionUpdatedEvent(
|
||||
event,
|
||||
params.onSubscriptionUpdated,
|
||||
);
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
return this.handleSubscriptionDeletedEvent(
|
||||
event,
|
||||
params.onSubscriptionDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
Logger.info(
|
||||
{
|
||||
eventType: event.type,
|
||||
name: this.namespace,
|
||||
},
|
||||
`Unhandled Stripe event type: ${event.type}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCheckoutSessionCompleted(
|
||||
event: Stripe.CheckoutSessionCompletedEvent,
|
||||
onCheckoutCompletedCallback: (
|
||||
data: InsertSubscriptionParams,
|
||||
customerId: string,
|
||||
) => Promise<unknown>,
|
||||
) {
|
||||
const stripe = await this.loadStripe();
|
||||
|
||||
const session = event.data.object;
|
||||
const subscriptionId = session.subscription as string;
|
||||
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
const accountId = session.client_reference_id!;
|
||||
const customerId = session.customer as string;
|
||||
|
||||
// TODO: support tiered pricing calculations
|
||||
// the amount total is amount in cents (e.g. 1000 = $10.00)
|
||||
// TODO: convert or store the amount in cents?
|
||||
const amount = session.amount_total ?? 0;
|
||||
|
||||
const payload = this.buildSubscriptionPayload<typeof accountId>({
|
||||
subscription,
|
||||
accountId,
|
||||
amount,
|
||||
});
|
||||
|
||||
return onCheckoutCompletedCallback(payload, customerId);
|
||||
}
|
||||
|
||||
private async handleSubscriptionUpdatedEvent(
|
||||
event: Stripe.CustomerSubscriptionUpdatedEvent,
|
||||
onSubscriptionUpdatedCallback: (
|
||||
data: Subscription['Update'],
|
||||
) => Promise<unknown>,
|
||||
) {
|
||||
const subscription = event.data.object;
|
||||
|
||||
const amount = subscription.items.data.reduce((acc, item) => {
|
||||
return (acc + (item.plan.amount ?? 0)) * (item.quantity ?? 1);
|
||||
}, 0);
|
||||
|
||||
const payload = this.buildSubscriptionPayload<undefined>({
|
||||
subscription,
|
||||
amount,
|
||||
});
|
||||
|
||||
return onSubscriptionUpdatedCallback(payload);
|
||||
}
|
||||
|
||||
private handleSubscriptionDeletedEvent(
|
||||
subscription: Stripe.CustomerSubscriptionDeletedEvent,
|
||||
onSubscriptionDeletedCallback: (subscriptionId: string) => Promise<unknown>,
|
||||
) {
|
||||
// Here we don't need to do anything, so we just return the callback
|
||||
|
||||
return onSubscriptionDeletedCallback(subscription.id);
|
||||
}
|
||||
|
||||
private buildSubscriptionPayload<
|
||||
AccountId extends string | undefined,
|
||||
>(params: {
|
||||
subscription: Stripe.Subscription;
|
||||
amount: number;
|
||||
// we only need the account id if we
|
||||
// are creating a subscription for an account
|
||||
accountId?: AccountId;
|
||||
}): AccountId extends string
|
||||
? InsertSubscriptionParams
|
||||
: Subscription['Update'] {
|
||||
const { subscription } = params;
|
||||
const lineItem = subscription.items.data[0];
|
||||
const price = lineItem?.price;
|
||||
const priceId = price?.id!;
|
||||
const interval = price?.recurring?.interval ?? null;
|
||||
|
||||
const data = {
|
||||
billing_provider: this.provider,
|
||||
id: subscription.id,
|
||||
status: subscription.status,
|
||||
price_amount: params.amount,
|
||||
cancel_at_period_end: subscription.cancel_at_period_end ?? false,
|
||||
interval: interval as string,
|
||||
currency: price?.currency!,
|
||||
product_id: price?.product as string,
|
||||
variant_id: priceId,
|
||||
interval_count: price?.recurring?.interval_count ?? 1,
|
||||
};
|
||||
|
||||
if (params.accountId !== undefined) {
|
||||
return {
|
||||
...data,
|
||||
account_id: params.accountId,
|
||||
} satisfies InsertSubscriptionParams;
|
||||
}
|
||||
|
||||
return data as Subscription['Update'];
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { invariant } from '@epic-web/invariant';
|
||||
import 'server-only';
|
||||
|
||||
const STRIPE_API_VERSION = '2023-10-16';
|
||||
|
||||
/**
|
||||
* @description returns a Stripe instance
|
||||
*/
|
||||
export async function createStripeClient() {
|
||||
const { default: Stripe } = await import('stripe');
|
||||
|
||||
invariant(
|
||||
process.env.STRIPE_SECRET_KEY,
|
||||
`'STRIPE_SECRET_KEY' environment variable was not provided`,
|
||||
);
|
||||
|
||||
return new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,6 @@
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-icons": "1.3.0",
|
||||
"@radix-ui/react-tooltip": "1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
@@ -30,14 +29,15 @@
|
||||
"react-top-loading-bar": "2.3.1",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"lucide-react": "0.307.0",
|
||||
"sonner": "^1.4.41",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"zod": "^3.22.4"
|
||||
"tailwind-merge": "^2.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"react-i18next": "^14.1.0"
|
||||
"react-i18next": "^14.1.0",
|
||||
"@radix-ui/react-icons": "1.3.0",
|
||||
"zod": "^3.22.4",
|
||||
"sonner": "^1.4.41",
|
||||
"lucide-react": "0.307.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "0.2.0",
|
||||
@@ -78,7 +78,6 @@
|
||||
"./dropdown-menu": "./src/shadcn/dropdown-menu.tsx",
|
||||
"./navigation-menu": "./src/shadcn/navigation-menu.tsx",
|
||||
"./form": "./src/shadcn/form.tsx",
|
||||
"./icons": "./src/shadcn/icons.tsx",
|
||||
"./input": "./src/shadcn/input.tsx",
|
||||
"./label": "./src/shadcn/label.tsx",
|
||||
"./popover": "./src/shadcn/popover.tsx",
|
||||
|
||||
@@ -5,12 +5,11 @@ import { useContext } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import Trans from '@/components/app/Trans';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import isRouteActive from '@kit/generic/is-route-active';
|
||||
|
||||
import Trans from '@/components/app/Trans';
|
||||
|
||||
import { NavigationMenuContext } from './navigation-menu-context';
|
||||
import { cn } from './utils';
|
||||
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import * as Lucide from 'lucide-react';
|
||||
import type { LucideProps } from 'lucide-react';
|
||||
|
||||
export type Icon = (props: LucideProps) => JSX.Element;
|
||||
|
||||
export const Logo = Lucide.Command;
|
||||
export const Dashboard = Lucide.Activity;
|
||||
export const Close = Lucide.X;
|
||||
export const Spinner = Lucide.Loader2;
|
||||
export const ChevronLeft = Lucide.ChevronLeft;
|
||||
export const ChevronRight = Lucide.ChevronRight;
|
||||
export const Trash = Lucide.Trash;
|
||||
export const Post = Lucide.FileText;
|
||||
export const Page = Lucide.File;
|
||||
export const Settings = Lucide.Settings;
|
||||
export const Billing = Lucide.CreditCard;
|
||||
export const Ellipsis = Lucide.MoreVertical;
|
||||
export const Organization = Lucide.Building;
|
||||
export const Add = Lucide.Plus;
|
||||
export const Warning = Lucide.AlertTriangle;
|
||||
export const User = Lucide.User;
|
||||
export const ArrowRight = Lucide.ArrowRight;
|
||||
export const Help = Lucide.HelpCircle;
|
||||
export const Twitter = Lucide.Twitter;
|
||||
export const Check = Lucide.Check;
|
||||
export const Copy = Lucide.Copy;
|
||||
export const CopyDone = Lucide.ClipboardCheck;
|
||||
export const Sun = Lucide.SunMedium;
|
||||
export const Moon = Lucide.Moon;
|
||||
export const Key = Lucide.Key;
|
||||
|
||||
export const System: Icon = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="m11.998 2c5.517 0 9.997 4.48 9.997 9.998 0 5.517-4.48 9.997-9.997 9.997-5.518 0-9.998-4.48-9.998-9.997 0-5.518 4.48-9.998 9.998-9.998zm0 1.5c-4.69 0-8.498 3.808-8.498 8.498s3.808 8.497 8.498 8.497z"
|
||||
fillRule="nonzero"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Mdx: Icon = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.97"
|
||||
height="24"
|
||||
viewBox="0 0 512 212"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m272.696 40.203l-.002 84.896l31.185-31.178l15.74 15.741l-57.642 57.638l-58.369-58.369l15.741-15.741l31.085 31.085l.001-84.072zM72.162 162.979V97.232l40.255 40.257l40.56-40.557v65.383h22.261V43.192l-62.82 62.816l-62.517-62.521v119.492z"
|
||||
/>
|
||||
<path
|
||||
fill="#F9AC00"
|
||||
d="m447.847 36.651l15.74 15.741l-47.149 47.147l45.699 45.701l-15.741 15.741l-45.7-45.699l-45.701 45.699l-15.74-15.741l45.695-45.701l-47.146-47.147l15.74-15.741l47.152 47.146z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ClerkWide: Icon = (props) => (
|
||||
<svg
|
||||
width="77"
|
||||
height="24"
|
||||
viewBox="0 0 77 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M35.1481 16.7381C34.7521 17.1486 34.2765 17.4741 33.7505 17.6947C33.2245 17.9154 32.659 18.0265 32.0886 18.0213C31.6069 18.0359 31.1273 17.9517 30.6794 17.7739C30.2315 17.5961 29.8247 17.3285 29.4841 16.9875C28.8654 16.3421 28.5093 15.4206 28.5093 14.3221C28.5093 12.1231 29.941 10.619 32.0886 10.619C32.6646 10.6109 33.2353 10.7301 33.7599 10.968C34.2845 11.206 34.7501 11.5568 35.1234 11.9955L36.9816 10.3525C35.7707 8.8827 33.8059 8.12305 31.9401 8.12305C28.2885 8.12305 25.6992 10.64 25.6992 14.343C25.6992 16.1745 26.3427 17.7167 27.4279 18.8057C28.5131 19.8947 30.0591 20.5344 31.843 20.5344C34.16 20.5344 36.0087 19.5939 37.0463 18.4116L35.1481 16.7381Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M38.7266 3.42773H41.4929V20.3398H38.7266V3.42773Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M54.8179 15.2828C54.8635 14.9145 54.8889 14.5439 54.894 14.1728C54.894 10.6659 52.5979 8.12611 49.0472 8.12611C48.2641 8.11071 47.4861 8.25581 46.7612 8.55246C46.0363 8.84911 45.3797 9.29104 44.832 9.85102C43.7944 10.94 43.1719 12.4822 43.1719 14.3213C43.1719 18.07 45.8144 20.5374 49.3176 20.5374C51.6688 20.5374 53.3614 19.5855 54.3762 18.2947L52.5637 16.6897L52.4742 16.6136C52.1146 17.0634 51.6561 17.4243 51.1344 17.6683C50.6127 17.9123 50.0419 18.0328 49.4661 18.0205C47.6879 18.0205 46.4046 16.9829 46.0391 15.2828H54.8179ZM46.0848 13.0628C46.2083 12.5269 46.4613 12.0295 46.8216 11.614C47.1214 11.2874 47.4883 11.0293 47.897 10.8574C48.3058 10.6856 48.7468 10.604 49.19 10.6183C50.7702 10.6183 51.7602 11.6064 52.101 13.0628H46.0848Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M63.445 8.08984V11.1741C63.1251 11.1494 62.8034 11.1246 62.6073 11.1246C60.513 11.1246 59.325 12.6287 59.325 14.603V20.3394H56.5625V8.2612H59.325V10.0908H59.3498C60.2884 8.80761 61.6344 8.09366 63.1004 8.09366L63.445 8.08984Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M69.8866 15.2812L67.8894 17.5031V20.3398H65.125V3.42773H67.8894V13.8019L72.8224 8.29975H76.1046L71.7638 13.1603L76.1808 20.3398H73.0718L69.938 15.2812H69.8866Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M19.116 3.1608L16.2354 6.04135C16.1449 6.13177 16.0266 6.18918 15.8996 6.20437C15.7725 6.21956 15.6441 6.19165 15.5348 6.12513C14.4017 5.44155 13.0949 5.10063 11.7722 5.14354C10.4495 5.18645 9.16759 5.61134 8.08114 6.36692C7.41295 6.83202 6.83276 7.41221 6.36765 8.0804C5.61297 9.16751 5.18848 10.4495 5.14524 11.7722C5.10201 13.0949 5.44187 14.4019 6.12395 15.536C6.19 15.6451 6.21764 15.7731 6.20246 15.8998C6.18728 16.0264 6.13015 16.1443 6.04018 16.2347L3.15962 19.1152C3.10162 19.1736 3.03168 19.2188 2.95459 19.2476C2.87751 19.2765 2.79511 19.2883 2.71302 19.2824C2.63093 19.2764 2.5511 19.2528 2.479 19.2131C2.40689 19.1734 2.34422 19.1186 2.29527 19.0524C0.736704 16.9101 -0.0687588 14.3121 0.0046021 11.6639C0.077963 9.01568 1.02602 6.46625 2.70079 4.41354C3.21208 3.78549 3.78622 3.21134 4.41428 2.70006C6.46683 1.02574 9.01589 0.0779624 11.6637 0.00460332C14.3115 -0.0687557 16.9091 0.736432 19.0512 2.29453C19.1179 2.34332 19.1731 2.40598 19.2131 2.47818C19.2532 2.55038 19.2771 2.6304 19.2833 2.71274C19.2895 2.79508 19.2777 2.87778 19.2488 2.95513C19.2199 3.03248 19.1746 3.10265 19.116 3.1608Z"
|
||||
fill="#5D31FF"
|
||||
/>
|
||||
<path
|
||||
d="M19.1135 20.8289L16.2329 17.9483C16.1424 17.8579 16.0241 17.8005 15.8971 17.7853C15.7701 17.7701 15.6416 17.798 15.5323 17.8645C14.4639 18.509 13.2398 18.8497 11.9921 18.8497C10.7443 18.8497 9.52022 18.509 8.45181 17.8645C8.34252 17.798 8.21406 17.7701 8.08701 17.7853C7.95997 17.8005 7.84171 17.8579 7.75119 17.9483L4.87063 20.8289C4.81022 20.8869 4.76333 20.9576 4.73329 21.0358C4.70324 21.114 4.69078 21.1979 4.69678 21.2815C4.70277 21.3651 4.72708 21.4463 4.76799 21.5194C4.80889 21.5926 4.86538 21.6558 4.93346 21.7046C6.98391 23.1965 9.45442 24.0001 11.9902 24.0001C14.5259 24.0001 16.9964 23.1965 19.0469 21.7046C19.1152 21.6561 19.172 21.5931 19.2133 21.5201C19.2545 21.4471 19.2792 21.366 19.2856 21.2824C19.2919 21.1988 19.2798 21.1148 19.2501 21.0365C19.2203 20.9581 19.1737 20.8872 19.1135 20.8289V20.8289Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M11.9973 15.4223C13.8899 15.4223 15.4243 13.888 15.4243 11.9953C15.4243 10.1027 13.8899 8.56836 11.9973 8.56836C10.1046 8.56836 8.57031 10.1027 8.57031 11.9953C8.57031 13.888 10.1046 15.4223 11.9973 15.4223Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TRPC: Icon = (props) => (
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect width="512" height="512" rx="150" fill="currentColor" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="fill-background"
|
||||
d="M255.446 75L326.523 116.008V138.556L412.554 188.238V273.224L435.631 286.546V368.608L364.6 409.615L333.065 391.378L256.392 435.646L180.178 391.634L149.085 409.615L78.0538 368.538V286.546L100.231 273.743V188.238L184.415 139.638L184.462 139.636V116.008L255.446 75ZM326.523 159.879V198.023L255.492 239.031L184.462 198.023V160.936L184.415 160.938L118.692 198.9V263.084L149.085 245.538L220.115 286.546V368.538L198.626 380.965L256.392 414.323L314.618 380.712L293.569 368.538V286.546L364.6 245.538L394.092 262.565V198.9L326.523 159.879ZM312.031 357.969V307.915L355.369 332.931V382.985L312.031 357.969ZM417.169 307.846L373.831 332.862V382.985L417.169 357.9V307.846ZM96.5154 357.9V307.846L139.854 332.862V382.915L96.5154 357.9ZM201.654 307.846L158.315 332.862V382.915L201.654 357.9V307.846ZM321.262 291.923L364.6 266.908L407.938 291.923L364.6 316.962L321.262 291.923ZM149.085 266.838L105.746 291.923L149.085 316.892L192.423 291.923L149.085 266.838ZM202.923 187.362V137.308L246.215 162.346V212.377L202.923 187.362ZM308.015 137.308L264.723 162.346V212.354L308.015 187.362V137.308ZM212.154 121.338L255.446 96.3231L298.785 121.338L255.446 146.354L212.154 121.338Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GitHub: Icon = (props) => (
|
||||
<svg viewBox="0 0 438.549 438.549" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const React: Icon = (props) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Nextjs: Icon = (props) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Prisma: Icon = (props) => (
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M21.8068 18.2848L13.5528.7565c-.207-.4382-.639-.7273-1.1286-.7541-.5023-.0293-.9523.213-1.2062.6253L2.266 15.1271c-.2773.4518-.2718 1.0091.0158 1.4555l4.3759 6.7786c.2608.4046.7127.6388 1.1823.6388.1332 0 .267-.0188.3987-.0577l12.7019-3.7568c.3891-.1151.7072-.3904.8737-.7553s.1633-.7828-.0075-1.1454zm-1.8481.7519L9.1814 22.2242c-.3292.0975-.6448-.1873-.5756-.5194l3.8501-18.4386c.072-.3448.5486-.3996.699-.0803l7.1288 15.138c.1344.2856-.019.6224-.325.7128z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Kysely: Icon = (props) => (
|
||||
<svg
|
||||
width="132"
|
||||
height="132"
|
||||
viewBox="0 0 132 132"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_8_3)">
|
||||
<rect x="2" y="2" width="128" height="128" rx="16" fill="white" />
|
||||
<path
|
||||
d="M41.2983 109V23.9091H46.4918V73.31H47.0735L91.9457 23.9091H98.8427L61.9062 64.1694L98.5103 109H92.0288L58.5824 67.9087L46.4918 81.2873V109H41.2983Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="128"
|
||||
height="128"
|
||||
rx="16"
|
||||
stroke="#121212"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<defs>
|
||||
<clipPath id="clip0_8_3">
|
||||
<rect x="2" y="2" width="128" height="128" rx="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Tailwind: Icon = (props) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Google: Icon = (props) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -45,6 +45,7 @@ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
const RadioGroupItemLabel = (
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
}>,
|
||||
) => {
|
||||
return (
|
||||
@@ -54,6 +55,9 @@ const RadioGroupItemLabel = (
|
||||
'flex cursor-pointer rounded-md' +
|
||||
' items-center space-x-4 border border-input hover:bg-muted' +
|
||||
' transition-duration-500 p-4 text-sm transition-colors focus-within:border-primary',
|
||||
{
|
||||
[`border-primary`]: props.selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from "@kit/ui/utils"
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
Reference in New Issue
Block a user