Change plan feedback modal (#1393)
* Checkbox component can have its label styles * Improved the dialog footer * Handle sending feedback to Slack using Plain * WIP making the modal conditional * Moved the Plain form action into the select plan file * removed comment * Show a confirmation diaglog if you’re downgrading from Pro to Hobby * Downgrading to Hobby works * Use redirectWithErrorMessage instead of throw error * The cancel form now submits the data correctly * Modals don’t trigger when you upgrade * Copy improvements * Added a tooltip to explain the link to the pricing page * Squashed commit of the following: commit886429b0edAuthor: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:36:46 2024 +0100 Removed emails, @trigger.dev/database and @trigger.dev/otlp-importer from changesets config commitf65157a14fAuthor: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:31:27 2024 +0100 Lockfile with run-engine removed commit3d67bb81e9Author: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:24:31 2024 +0100 Removed run-engine from the webapp package.json/tsconfig commitd30e9713f4Author: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:06:04 2024 +0100 Dockerfile fix because the database package has been moved commitf2babbf637Author: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 09:41:22 2024 -0700 Internal packages (testcontainers, redis-worker and zod-worker) (#1392) * Some notes on the new run engine * lockfile with setup for the run engine * Documenting where TaskRun is currently mutated, to try figure out the shape of the new system * Added notes about how triggering currently works * Details about when triggering happens * Lots of notes about waitpoints * Started scaffolding the RunEngine * Sketch of Prisma waitpoint schema while it’s fresh in my mind * Got Prisma working with testcontainers * Use beforeEach/afterEach * Simple Prisma and Redis test * Return Redis options instead of a client * Simplified things * A very simple FIFO pull-based queue to check the tests working properly * Use vitest extend * Separate redis, postgres and combined tests for faster testing * Some fixes and test improvements * Pass a logger into the queue * A queue processor that processes items from the given queue as fast as it can * Test for retrying an item that wasn’t processed * First draft of waitpoints in the Prisma schema * Remove the custom logger from the test * Added a completedAt to Waitpoint * Notes on the flow for an execution starting * Added redlock, moved some files around * Starting point for the TaskRunExecutionSnapshot table * Added relationships to TaskRunExecutionSnapshot * Change some tsconfig * Moved some things around * Added some packages * WIP on the RunQueue * Fix for some imports * Key producer with some tests * Removed the nv type from the keys… it’s not useful to do global queries * Passing unit tests for all the public key producer functions * Some basic tests passing for the RunQueue * Simple enqueue test working * Enqueue and dequeue for dev is working * Don’t log everything during the tests * Enqueuing/dequeuing from the shared queue is working * Tests for getting a shared queue * The key producer sharedQueue can now be named, to allow multiple separate queues * The key producer uses the name of the queue as the input * Extra info in the Prisma schema * Dequeuing a message gets the payload and sets the task concurrency all in one Lua script * Adding more keys so we can read the concurrency from the queue * Setting the concurrency with dequeue and enquque is working * Improved the tests and fixed some bugs * Acking is resetting the concurrencies * Check the key has been removed after acking * Nacking is working * Changed the package to CommonJS + Node10 so it works with Redlock * Moved the database, otel and emails packages to be in internal-packages * Moved some Prisma code to the database package * Started using the RunEngine for triggering * Progress on run engine triggering, first waitpoint code * Create a delay waitpoint * Moved ZodWorker to an internal package so it can be used in the run engine as well as the webapp * Web app now uses the zod worker package * Added parseNaturalLanguageDuration to core/apps * internal-packages/zod-worker in the lockfile * Pass in the master queue, remove old rebalance workers code * Add masterQueue to TaskRun * Fixed the tests * Moved waitpoint code into the run engine, also the zod worker * Completing waitpoints * An experiment to create a new test container with environment * More changes to triggering * Started testing triggering * Test for a run getting triggered and being enqueued * Removed dequeueMessageInEnv * Update dev queue tests to use the shared queue function * Schema changes for TaskRunExecutionSnapshot * First execution snapshot when the run is created. Dequeue run function added to the engine * Separate internal package for testcontainers so they can be used elsewhere * Remove the simple queue and testcontainers from the run-engine. They’re going to be separate * Fix for the wrong path to the Prisma schem,a * Added the testcontainers package to the run-engine * redis-worker package, just a copy of the simple queue for now * The queue now uses Lua to enqueue dequeue * The queue now has a catalog and an invisible period after dequeuing * Added a visibility timeout and acking, with tests * Added more Redis connection logging, deleted todos * Visibility timeouts are now defined on the catalog and can be overridden when enqueuing * Dequeue multiple items at once * Test for dequeuing multiple items * Export some types to be used elsewhere * Partial refactor of the processor * First stab at a worker with concurrency and NodeWorkers * Don’t have a default visibility timeout in the queue * Worker setup and processing items in a simple test * Process jobs in parallel with retrying * Get the attempt when dequeuing * Workers do exponential backoff * Moved todos * DLQ functionality * DLQ tests * Same cluster for all keys in the same queue * Added DLQ tests * Whitespace * Redis pubsub to redrive from the worker * Fixed database paths * Fix for path to zod-worker * Fixes for typecheck errors, mostly with TS versions and module resolution * Redlock required a patch * Moved the new DB migrations to the new database package folder * Remove the run-engine package * Remove the RunEngine prisma schema changes * Delete triggerTaskV2 * Remove zodworker test script (no tests) * Update test-containers readme * Generate the client first * Use a specific version of the prisma package * Generate the prisma client before running the unit tests commitfc609470c4Author: Dan <8297864+D-K-P@users.noreply.github.com> Date: Tue Oct 8 14:36:03 2024 +0100 Supabase database webhook example upgrade (#1386) * Added overview for guides and examples section and split them all out * New supabase guide wip * Updated images and improved docs * Trimmed the supabase prereqs * Supabase guide wip * more updates * Replaced old database webhook guide * Created one intro page and removed snippets * Updated guide sidebar titles * Code updates * More improvements * Updates and added images * Compressed image * Updated guides descriptions and edge function basic * Removed bold * Updated redirects * Fixed broken links * Updated intro commit07f82eae54Author: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 13:28:54 2024 +0100 Release 3.0.11 commit13ebfcc5b9Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue Oct 8 13:24:38 2024 +0100 chore: Update version for release (#1381) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> commit2a04d17e1fAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 09:24:23 2024 +0100 Simplify showLogs expression commit002ae4bc0dAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 09:22:29 2024 +0100 Fix dotenv overrides for dev runs (#1388) * override dashboard dev env vars with local .env * add changeset * add simple task for testing env vars commit047cb00417Author: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 09:22:05 2024 +0100 Disable schedules for deleted orgs on next tick (#1383) * disable schedules for deleted orgs * add debug logs commit2c014f725aAuthor: James Ritchie <james@trigger.dev> Date: Sun Oct 6 13:02:00 2024 -0700 Override log retention (#1385) * set full log retention as admin * If run.logsDeletedAt is set, don’t bother getting the trace commita69e04f03aAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat Oct 5 14:18:58 2024 +0100 Include push output in logs for self-hosted deploys (#1382) * include push output in logs * changeset commitc5488df8caAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat Oct 5 13:12:47 2024 +0100 Fix CLI downgrade check (#1380) * fix downgrade detection * remove unused semver package from webapp * add changeset commit1caec27f72Author: Eric Allam <eric@trigger.dev> Date: Fri Oct 4 15:33:35 2024 -0700 docs: Max duration (#1379) * maxDuration docs * Update the init command to set the maxDuration and include a commented out maxDuration in the config file commite14c954e12Author: Eric Allam <eallam@icloud.com> Date: Fri Oct 4 15:02:05 2024 -0700 Release 3.0.10 commit8e61f5d631Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri Oct 4 14:59:07 2024 -0700 chore: Update version for release (#1378) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> commit08db565a72Author: Eric Allam <eallam@icloud.com> Date: Thu Oct 3 12:38:25 2024 -0700 improve the timed out description commit6d0884254eAuthor: Eric Allam <eric@trigger.dev> Date: Thu Oct 3 12:43:26 2024 -0700 feat: Add maxDuration to tasks (#1377) * WIP * Get max duration working on deployed runs * Actually set the timed out runs to status = TIMED_OUT * The client status for TIMED_OUT is now MAX_DURATION_EXCEEDED * New TimedOutIcon * Added new timedout icon * Add ability to opt-out of maxDuration with timeout.None * MAX_DURATION_EXCEEDED -> TIMED_OUT * changeset * Improved styling for the status tooltip content --------- Co-authored-by: James Ritchie <james@trigger.dev> commit665ccf88b7Author: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu Oct 3 12:33:18 2024 +0100 Update github actions and self-hosting docs commit1ff7b861caAuthor: Eric Allam <eric@trigger.dev> Date: Wed Oct 2 18:26:36 2024 -0700 Add max queue depth limits (#1376) * Add runs to an env queue, as well as the actual queue * Add queue size limit guard on triggering tasks commitc531a9d986Author: Eric Allam <eric@trigger.dev> Date: Wed Oct 2 15:30:39 2024 -0700 fix: cleanup ttl expire run graphile jobs (#1373) * fix: remove ttl expire run graphile jobs when a run is started or completed * Update expireEnqueuedRun.server.ts commit0bf500fa95Author: Matt Aitken <matt@mattaitken.com> Date: Wed Oct 2 15:30:16 2024 -0700 Prioritize finishing waited runs (#1375) * If a tree node is missing, estimate the size as zero * Task to test prioritizing finishing existing runs after triggerAndWaits * When requeuing a run with a checkpoint, put it in the queue with the parent run time so it’s correctly prioritized * The same change but if there’s no checkpoint * Squashed commit of the following: commit886429b0edAuthor: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:36:46 2024 +0100 Removed emails, @trigger.dev/database and @trigger.dev/otlp-importer from changesets config commitf65157a14fAuthor: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:31:27 2024 +0100 Lockfile with run-engine removed commit3d67bb81e9Author: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:24:31 2024 +0100 Removed run-engine from the webapp package.json/tsconfig commitd30e9713f4Author: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 18:06:04 2024 +0100 Dockerfile fix because the database package has been moved commitf2babbf637Author: Matt Aitken <matt@mattaitken.com> Date: Tue Oct 8 09:41:22 2024 -0700 Internal packages (testcontainers, redis-worker and zod-worker) (#1392) * Some notes on the new run engine * lockfile with setup for the run engine * Documenting where TaskRun is currently mutated, to try figure out the shape of the new system * Added notes about how triggering currently works * Details about when triggering happens * Lots of notes about waitpoints * Started scaffolding the RunEngine * Sketch of Prisma waitpoint schema while it’s fresh in my mind * Got Prisma working with testcontainers * Use beforeEach/afterEach * Simple Prisma and Redis test * Return Redis options instead of a client * Simplified things * A very simple FIFO pull-based queue to check the tests working properly * Use vitest extend * Separate redis, postgres and combined tests for faster testing * Some fixes and test improvements * Pass a logger into the queue * A queue processor that processes items from the given queue as fast as it can * Test for retrying an item that wasn’t processed * First draft of waitpoints in the Prisma schema * Remove the custom logger from the test * Added a completedAt to Waitpoint * Notes on the flow for an execution starting * Added redlock, moved some files around * Starting point for the TaskRunExecutionSnapshot table * Added relationships to TaskRunExecutionSnapshot * Change some tsconfig * Moved some things around * Added some packages * WIP on the RunQueue * Fix for some imports * Key producer with some tests * Removed the nv type from the keys… it’s not useful to do global queries * Passing unit tests for all the public key producer functions * Some basic tests passing for the RunQueue * Simple enqueue test working * Enqueue and dequeue for dev is working * Don’t log everything during the tests * Enqueuing/dequeuing from the shared queue is working * Tests for getting a shared queue * The key producer sharedQueue can now be named, to allow multiple separate queues * The key producer uses the name of the queue as the input * Extra info in the Prisma schema * Dequeuing a message gets the payload and sets the task concurrency all in one Lua script * Adding more keys so we can read the concurrency from the queue * Setting the concurrency with dequeue and enquque is working * Improved the tests and fixed some bugs * Acking is resetting the concurrencies * Check the key has been removed after acking * Nacking is working * Changed the package to CommonJS + Node10 so it works with Redlock * Moved the database, otel and emails packages to be in internal-packages * Moved some Prisma code to the database package * Started using the RunEngine for triggering * Progress on run engine triggering, first waitpoint code * Create a delay waitpoint * Moved ZodWorker to an internal package so it can be used in the run engine as well as the webapp * Web app now uses the zod worker package * Added parseNaturalLanguageDuration to core/apps * internal-packages/zod-worker in the lockfile * Pass in the master queue, remove old rebalance workers code * Add masterQueue to TaskRun * Fixed the tests * Moved waitpoint code into the run engine, also the zod worker * Completing waitpoints * An experiment to create a new test container with environment * More changes to triggering * Started testing triggering * Test for a run getting triggered and being enqueued * Removed dequeueMessageInEnv * Update dev queue tests to use the shared queue function * Schema changes for TaskRunExecutionSnapshot * First execution snapshot when the run is created. Dequeue run function added to the engine * Separate internal package for testcontainers so they can be used elsewhere * Remove the simple queue and testcontainers from the run-engine. They’re going to be separate * Fix for the wrong path to the Prisma schem,a * Added the testcontainers package to the run-engine * redis-worker package, just a copy of the simple queue for now * The queue now uses Lua to enqueue dequeue * The queue now has a catalog and an invisible period after dequeuing * Added a visibility timeout and acking, with tests * Added more Redis connection logging, deleted todos * Visibility timeouts are now defined on the catalog and can be overridden when enqueuing * Dequeue multiple items at once * Test for dequeuing multiple items * Export some types to be used elsewhere * Partial refactor of the processor * First stab at a worker with concurrency and NodeWorkers * Don’t have a default visibility timeout in the queue * Worker setup and processing items in a simple test * Process jobs in parallel with retrying * Get the attempt when dequeuing * Workers do exponential backoff * Moved todos * DLQ functionality * DLQ tests * Same cluster for all keys in the same queue * Added DLQ tests * Whitespace * Redis pubsub to redrive from the worker * Fixed database paths * Fix for path to zod-worker * Fixes for typecheck errors, mostly with TS versions and module resolution * Redlock required a patch * Moved the new DB migrations to the new database package folder * Remove the run-engine package * Remove the RunEngine prisma schema changes * Delete triggerTaskV2 * Remove zodworker test script (no tests) * Update test-containers readme * Generate the client first * Use a specific version of the prisma package * Generate the prisma client before running the unit tests commitfc609470c4Author: Dan <8297864+D-K-P@users.noreply.github.com> Date: Tue Oct 8 14:36:03 2024 +0100 Supabase database webhook example upgrade (#1386) * Added overview for guides and examples section and split them all out * New supabase guide wip * Updated images and improved docs * Trimmed the supabase prereqs * Supabase guide wip * more updates * Replaced old database webhook guide * Created one intro page and removed snippets * Updated guide sidebar titles * Code updates * More improvements * Updates and added images * Compressed image * Updated guides descriptions and edge function basic * Removed bold * Updated redirects * Fixed broken links * Updated intro commit07f82eae54Author: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 13:28:54 2024 +0100 Release 3.0.11 commit13ebfcc5b9Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue Oct 8 13:24:38 2024 +0100 chore: Update version for release (#1381) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> commit2a04d17e1fAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 09:24:23 2024 +0100 Simplify showLogs expression commit002ae4bc0dAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 09:22:29 2024 +0100 Fix dotenv overrides for dev runs (#1388) * override dashboard dev env vars with local .env * add changeset * add simple task for testing env vars commit047cb00417Author: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue Oct 8 09:22:05 2024 +0100 Disable schedules for deleted orgs on next tick (#1383) * disable schedules for deleted orgs * add debug logs commit2c014f725aAuthor: James Ritchie <james@trigger.dev> Date: Sun Oct 6 13:02:00 2024 -0700 Override log retention (#1385) * set full log retention as admin * If run.logsDeletedAt is set, don’t bother getting the trace commita69e04f03aAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat Oct 5 14:18:58 2024 +0100 Include push output in logs for self-hosted deploys (#1382) * include push output in logs * changeset commitc5488df8caAuthor: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat Oct 5 13:12:47 2024 +0100 Fix CLI downgrade check (#1380) * fix downgrade detection * remove unused semver package from webapp * add changeset commit1caec27f72Author: Eric Allam <eric@trigger.dev> Date: Fri Oct 4 15:33:35 2024 -0700 docs: Max duration (#1379) * maxDuration docs * Update the init command to set the maxDuration and include a commented out maxDuration in the config file commite14c954e12Author: Eric Allam <eallam@icloud.com> Date: Fri Oct 4 15:02:05 2024 -0700 Release 3.0.10 commit8e61f5d631Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri Oct 4 14:59:07 2024 -0700 chore: Update version for release (#1378) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> commit08db565a72Author: Eric Allam <eallam@icloud.com> Date: Thu Oct 3 12:38:25 2024 -0700 improve the timed out description commit6d0884254eAuthor: Eric Allam <eric@trigger.dev> Date: Thu Oct 3 12:43:26 2024 -0700 feat: Add maxDuration to tasks (#1377) * WIP * Get max duration working on deployed runs * Actually set the timed out runs to status = TIMED_OUT * The client status for TIMED_OUT is now MAX_DURATION_EXCEEDED * New TimedOutIcon * Added new timedout icon * Add ability to opt-out of maxDuration with timeout.None * MAX_DURATION_EXCEEDED -> TIMED_OUT * changeset * Improved styling for the status tooltip content --------- Co-authored-by: James Ritchie <james@trigger.dev> commit665ccf88b7Author: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu Oct 3 12:33:18 2024 +0100 Update github actions and self-hosting docs commit1ff7b861caAuthor: Eric Allam <eric@trigger.dev> Date: Wed Oct 2 18:26:36 2024 -0700 Add max queue depth limits (#1376) * Add runs to an env queue, as well as the actual queue * Add queue size limit guard on triggering tasks commitc531a9d986Author: Eric Allam <eric@trigger.dev> Date: Wed Oct 2 15:30:39 2024 -0700 fix: cleanup ttl expire run graphile jobs (#1373) * fix: remove ttl expire run graphile jobs when a run is started or completed * Update expireEnqueuedRun.server.ts commit0bf500fa95Author: Matt Aitken <matt@mattaitken.com> Date: Wed Oct 2 15:30:16 2024 -0700 Prioritize finishing waited runs (#1375) * If a tree node is missing, estimate the size as zero * Task to test prioritizing finishing existing runs after triggerAndWaits * When requeuing a run with a checkpoint, put it in the queue with the parent run time so it’s correctly prioritized * The same change but if there’s no checkpoint * Revert "Squashed commit of the following:" This reverts commit b837b5a558dc5187d89b09135276d16f6856f455. * Removed console logs * cleaned up conditionals * Unlock free plan state * Fixed subscribe button if you’re already github verified * Simplified the downgrade reasons logic * made periodEnd required --------- Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
This commit is contained in:
@@ -61,6 +61,7 @@ export type CheckboxProps = Omit<
|
||||
description?: string;
|
||||
badges?: string[];
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
onChange?: (isChecked: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -78,6 +79,7 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
|
||||
badges,
|
||||
disabled,
|
||||
className,
|
||||
labelClassName: externalLabelClassName,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -148,7 +150,8 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
|
||||
labelClassName
|
||||
labelClassName,
|
||||
externalLabelClassName
|
||||
)}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -82,7 +82,7 @@ DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-between sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -117,6 +117,7 @@ export default function ChoosePlanPage() {
|
||||
subscription={v3Subscription}
|
||||
organizationSlug={organizationSlug}
|
||||
hasPromotedPlan={false}
|
||||
periodEnd={periodEnd}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { z } from "zod";
|
||||
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
||||
import { MainBody } from "~/components/layout/AppLayout";
|
||||
import { SideMenu } from "~/components/navigation/SideMenu";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { useOptionalOrganization } from "~/hooks/useOrganizations";
|
||||
import { useTypedMatchesData } from "~/hooks/useTypedMatchData";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
|
||||
@@ -37,11 +37,15 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
|
||||
const currentPlan = await getCurrentPlan(organization.id);
|
||||
|
||||
return typedjson({ ...plans, ...currentPlan, organizationSlug });
|
||||
const periodEnd = new Date();
|
||||
periodEnd.setMonth(periodEnd.getMonth() + 1);
|
||||
|
||||
return typedjson({ ...plans, ...currentPlan, organizationSlug, periodEnd });
|
||||
}
|
||||
|
||||
export default function ChoosePlanPage() {
|
||||
const { plans, v3Subscription, organizationSlug } = useTypedLoaderData<typeof loader>();
|
||||
const { plans, v3Subscription, organizationSlug, periodEnd } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<MainCenteredContainer className="flex max-w-[80rem] flex-col items-center gap-8 p-3">
|
||||
@@ -52,6 +56,7 @@ export default function ChoosePlanPage() {
|
||||
organizationSlug={organizationSlug}
|
||||
hasPromotedPlan
|
||||
showGithubVerificationBadge
|
||||
periodEnd={periodEnd}
|
||||
/>
|
||||
</MainCenteredContainer>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
CheckIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ShieldCheckIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { ArrowDownCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { PlainClient, uiComponent } from "@team-plain/typescript-sdk";
|
||||
import { GitHubLightIcon } from "@trigger.dev/companyicons";
|
||||
import {
|
||||
FreePlanDefinition,
|
||||
Limits,
|
||||
@@ -14,11 +18,14 @@ import {
|
||||
SetPlanBody,
|
||||
SubscriptionResult,
|
||||
} from "@trigger.dev/platform/v3";
|
||||
import { GitHubLightIcon } from "@trigger.dev/companyicons";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { inspect } from "util";
|
||||
import { z } from "zod";
|
||||
import { DefinitionTip } from "~/components/DefinitionTooltip";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -26,13 +33,17 @@ import {
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { TextArea } from "~/components/primitives/TextArea";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { redirectWithErrorMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { setPlan } from "~/services/platform.v3.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const Params = z.object({
|
||||
@@ -43,6 +54,8 @@ const schema = z.object({
|
||||
type: z.enum(["free", "paid"]),
|
||||
planCode: z.string().optional(),
|
||||
callerPath: z.string(),
|
||||
reasons: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
@@ -51,11 +64,16 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
const { organizationSlug } = Params.parse(params);
|
||||
const user = await requireUser(request);
|
||||
const formData = await request.formData();
|
||||
const reasons = formData.getAll("reasons");
|
||||
const message = formData.get("message");
|
||||
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = Object.fromEntries(await request.formData());
|
||||
const form = schema.parse(formData);
|
||||
const form = schema.parse({
|
||||
...Object.fromEntries(formData),
|
||||
reasons,
|
||||
message: message || undefined,
|
||||
});
|
||||
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { slug: organizationSlug },
|
||||
@@ -69,9 +87,106 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
|
||||
switch (form.type) {
|
||||
case "free": {
|
||||
try {
|
||||
if (!env.PLAIN_API_KEY) {
|
||||
throw new Error("PLAIN_API_KEY is not set");
|
||||
}
|
||||
|
||||
const client = new PlainClient({
|
||||
apiKey: env.PLAIN_API_KEY,
|
||||
});
|
||||
|
||||
const upsertCustomerRes = await client.upsertCustomer({
|
||||
identifier: {
|
||||
emailAddress: user.email,
|
||||
},
|
||||
onCreate: {
|
||||
externalId: user.id,
|
||||
fullName: user.name ?? "",
|
||||
email: {
|
||||
email: user.email,
|
||||
isVerified: true,
|
||||
},
|
||||
},
|
||||
onUpdate: {
|
||||
externalId: { value: user.id },
|
||||
fullName: { value: user.name ?? "" },
|
||||
email: {
|
||||
email: user.email,
|
||||
isVerified: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (upsertCustomerRes.error) {
|
||||
console.error(
|
||||
inspect(upsertCustomerRes.error, {
|
||||
showHidden: false,
|
||||
depth: null,
|
||||
colors: true,
|
||||
})
|
||||
);
|
||||
throw redirectWithErrorMessage(form.callerPath, request, upsertCustomerRes.error.message);
|
||||
}
|
||||
|
||||
// Only create a thread if there are reasons or a message
|
||||
if (reasons.length > 0 || (message && message.toString().trim() !== "")) {
|
||||
const createThreadRes = await client.createThread({
|
||||
customerIdentifier: {
|
||||
customerId: upsertCustomerRes.data.customer.id,
|
||||
},
|
||||
title: "Plan cancelation feedback",
|
||||
components: [
|
||||
uiComponent.text({
|
||||
text: `${user.name} (${user.email}) just canceled their plan.`,
|
||||
}),
|
||||
uiComponent.divider({ spacingSize: "M" }),
|
||||
...(reasons.length > 0
|
||||
? [
|
||||
uiComponent.spacer({ size: "L" }),
|
||||
uiComponent.text({
|
||||
size: "L",
|
||||
color: "NORMAL",
|
||||
text: "Reasons:",
|
||||
}),
|
||||
uiComponent.text({
|
||||
text: reasons.join(", "),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(message
|
||||
? [
|
||||
uiComponent.spacer({ size: "L" }),
|
||||
uiComponent.text({
|
||||
size: "L",
|
||||
color: "NORMAL",
|
||||
text: "Comment:",
|
||||
}),
|
||||
uiComponent.text({
|
||||
text: message.toString(),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
if (createThreadRes.error) {
|
||||
console.error(
|
||||
inspect(createThreadRes.error, {
|
||||
showHidden: false,
|
||||
depth: null,
|
||||
colors: true,
|
||||
})
|
||||
);
|
||||
throw redirectWithErrorMessage(form.callerPath, request, createThreadRes.error.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to submit to Plain the unsubscribe reason", { error: e });
|
||||
}
|
||||
payload = {
|
||||
type: "free" as const,
|
||||
userId,
|
||||
userId: user.id,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -82,10 +197,13 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
payload = {
|
||||
type: "paid" as const,
|
||||
planCode: form.planCode,
|
||||
userId,
|
||||
userId: user.id,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Invalid form type");
|
||||
}
|
||||
}
|
||||
|
||||
return setPlan(organization, request, form.callerPath, payload);
|
||||
@@ -134,6 +252,7 @@ type PricingPlansProps = {
|
||||
organizationSlug: string;
|
||||
hasPromotedPlan: boolean;
|
||||
showGithubVerificationBadge?: boolean;
|
||||
periodEnd: Date;
|
||||
};
|
||||
|
||||
export function PricingPlans({
|
||||
@@ -142,6 +261,7 @@ export function PricingPlans({
|
||||
organizationSlug,
|
||||
hasPromotedPlan,
|
||||
showGithubVerificationBadge,
|
||||
periodEnd,
|
||||
}: PricingPlansProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
@@ -151,6 +271,7 @@ export function PricingPlans({
|
||||
subscription={subscription}
|
||||
organizationSlug={organizationSlug}
|
||||
showGithubVerificationBadge={showGithubVerificationBadge}
|
||||
periodEnd={periodEnd}
|
||||
/>
|
||||
<TierHobby
|
||||
plan={plans.hobby}
|
||||
@@ -172,23 +293,33 @@ export function TierFree({
|
||||
subscription,
|
||||
organizationSlug,
|
||||
showGithubVerificationBadge,
|
||||
periodEnd,
|
||||
}: {
|
||||
plan: FreePlanDefinition;
|
||||
subscription?: SubscriptionResult;
|
||||
organizationSlug: string;
|
||||
showGithubVerificationBadge?: boolean;
|
||||
periodEnd: Date;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const navigation = useNavigation();
|
||||
const formAction = `/resources/orgs/${organizationSlug}/select-plan`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isLackingFeaturesChecked, setIsLackingFeaturesChecked] = useState(false);
|
||||
const status = subscription?.freeTierStatus ?? "requires_connect";
|
||||
|
||||
useEffect(() => {
|
||||
setIsDialogOpen(false);
|
||||
}, [subscription]);
|
||||
|
||||
return (
|
||||
<TierContainer>
|
||||
<div className="relative">
|
||||
<PricingHeader title={plan.title} cost={0} />
|
||||
<TierLimit href="https://trigger.dev/pricing#computePricing">
|
||||
${plan.limits.includedUsage / 100} free monthly usage
|
||||
</TierLimit>
|
||||
{showGithubVerificationBadge && status === "approved" && (
|
||||
<SimpleTooltip
|
||||
buttonClassName="absolute right-1 top-1"
|
||||
@@ -242,16 +373,11 @@ export function TierFree({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Form action={formAction} method="post" id="subscribe">
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<TierLimit href="https://trigger.dev/pricing#computePricing">
|
||||
${plan.limits.includedUsage / 100} free usage
|
||||
</TierLimit>
|
||||
<div className="py-6">
|
||||
{status === "requires_connect" ? (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<>
|
||||
{status === "requires_connect" ? (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<div className="my-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="tertiary/large"
|
||||
@@ -260,12 +386,16 @@ export function TierFree({
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? Spinner : undefined}
|
||||
>
|
||||
Unlock free plan
|
||||
Unlock Free plan
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<Form action={formAction} method="post" id="subscribe-free">
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<DialogHeader>Unlock the Free plan</DialogHeader>
|
||||
<div className="mb-3 mt-4 flex flex-col items-center gap-4 px-6">
|
||||
<div className="mb-5 mt-7 flex flex-col items-center gap-4 px-6">
|
||||
<GitHubLightIcon className="size-16" />
|
||||
<Paragraph variant="base/bright" className="text-center">
|
||||
To unlock the Free plan, we need to verify that you have an active GitHub
|
||||
@@ -282,16 +412,104 @@ export function TierFree({
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? Spinner : undefined}
|
||||
form="subscribe"
|
||||
form="subscribe-free"
|
||||
>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : subscription?.plan !== undefined &&
|
||||
subscription.plan.type !== "free" &&
|
||||
subscription.canceledAt === undefined ? (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen} key="cancel">
|
||||
<DialogTrigger asChild>
|
||||
<div className="my-6">
|
||||
<Button variant="tertiary/large" fullWidth className="text-md font-medium">
|
||||
{`Downgrade to ${plan.title}`}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<Form action={formAction} method="post" id="subscribe">
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<DialogHeader>Downgrade plan?</DialogHeader>
|
||||
<div className="flex items-start gap-3 pb-6 pr-2 pt-8">
|
||||
<ArrowDownCircleIcon className="size-12 min-w-12 text-error" />
|
||||
<Paragraph variant="base/bright" className="text-text-bright">
|
||||
Are you sure you want to downgrade? You will lose access to your current
|
||||
plan's features on{" "}
|
||||
<DateTime
|
||||
includeTime={false}
|
||||
date={new Date(periodEnd.getTime() + 86400000)}
|
||||
/>
|
||||
.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Header2 className="mb-1">Why are you thinking of downgrading?</Header2>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Subscription or usage costs too expensive",
|
||||
"Bugs or technical issues",
|
||||
"No longer need the service",
|
||||
"Found a better alternative",
|
||||
"Lacking features I need",
|
||||
].map((label, index) => (
|
||||
<li key={index}>
|
||||
<CheckboxWithLabel
|
||||
id={`reason-${index + 1}`}
|
||||
name="reasons"
|
||||
value={label}
|
||||
variant="simple"
|
||||
label={label}
|
||||
labelClassName="text-text-dimmed"
|
||||
onChange={(isChecked: boolean) => {
|
||||
if (label === "Lacking features I need") {
|
||||
setIsLackingFeaturesChecked(isChecked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<Header2 className="mb-1">
|
||||
{isLackingFeaturesChecked
|
||||
? "What features do you need? Or how can we improve?"
|
||||
: "What can we do to improve?"}
|
||||
</Header2>
|
||||
<TextArea id="improvement-suggestions" name="message" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="mt-2">
|
||||
<Button variant="tertiary/medium" onClick={() => setIsDialogOpen(false)}>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger/medium"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? () => <Spinner color="white" /> : undefined}
|
||||
type="submit"
|
||||
>
|
||||
Downgrade plan
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Form action={formAction} method="post" id="subscribe-verified" className="my-6">
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<Button
|
||||
variant="tertiary/large"
|
||||
type="submit"
|
||||
form="subscribe-verified"
|
||||
fullWidth
|
||||
className="text-md font-medium"
|
||||
disabled={
|
||||
@@ -305,12 +523,14 @@ export function TierFree({
|
||||
>
|
||||
{subscription?.plan === undefined
|
||||
? "Select plan"
|
||||
: subscription.plan.type === "free" || subscription.canceledAt !== undefined
|
||||
: subscription.plan.type === "free"
|
||||
? "Current plan"
|
||||
: `Downgrade to ${plan.title}`}
|
||||
: subscription.canceledAt !== undefined
|
||||
? "Current plan"
|
||||
: "Select plan"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<ConcurrentRuns limits={plan.limits} />
|
||||
<FeatureItem checked>
|
||||
@@ -329,7 +549,7 @@ export function TierFree({
|
||||
<SupportLevel limits={plan.limits} />
|
||||
<Alerts limits={plan.limits} />
|
||||
</ul>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</TierContainer>
|
||||
);
|
||||
@@ -350,6 +570,11 @@ export function TierHobby({
|
||||
const navigation = useNavigation();
|
||||
const formAction = `/resources/orgs/${organizationSlug}/select-plan`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDialogOpen(false);
|
||||
}, [subscription]);
|
||||
|
||||
return (
|
||||
<TierContainer isHighlighted={isHighlighted}>
|
||||
@@ -357,15 +582,52 @@ export function TierHobby({
|
||||
<TierLimit href="https://trigger.dev/pricing#computePricing">
|
||||
${plan.limits.includedUsage / 100} usage included
|
||||
</TierLimit>
|
||||
<Form action={formAction} method="post" id="subscribe">
|
||||
<div className="py-6">
|
||||
<input type="hidden" name="type" value="paid" />
|
||||
<input type="hidden" name="planCode" value={plan.code} />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<Form action={formAction} method="post" id="subscribe-hobby" className="py-6">
|
||||
<input type="hidden" name="type" value="paid" />
|
||||
<input type="hidden" name="planCode" value={plan.code} />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
{subscription?.plan !== undefined &&
|
||||
subscription.plan.type !== "free" &&
|
||||
subscription.canceledAt === undefined &&
|
||||
subscription.plan.code !== plan.code ? (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen} key="downgrade">
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="tertiary/large" fullWidth className="text-md font-medium">
|
||||
{`Downgrade to ${plan.title}`}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>Downgrade plan?</DialogHeader>
|
||||
<div className="mb-2 mt-4 flex items-start gap-3">
|
||||
<span>
|
||||
<ArrowDownCircleIcon className="size-12 text-blue-500" />
|
||||
</span>
|
||||
<Paragraph variant="base/bright" className="text-text-bright">
|
||||
By downgrading you will lose access to your current plan's features and your
|
||||
included credits will be reduced.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="tertiary/medium" onClick={() => setIsDialogOpen(false)}>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? () => <Spinner color="white" /> : undefined}
|
||||
form="subscribe-hobby"
|
||||
>
|
||||
{`Downgrade to ${plan.title}`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Button
|
||||
variant={isHighlighted ? "primary/large" : "tertiary/large"}
|
||||
fullWidth
|
||||
className="text-md font-medium"
|
||||
form="subscribe-hobby"
|
||||
disabled={
|
||||
isLoading ||
|
||||
(subscription?.plan?.code === plan.code && subscription.canceledAt === undefined)
|
||||
@@ -380,9 +642,9 @@ export function TierHobby({
|
||||
? `Upgrade to ${plan.title}`
|
||||
: subscription.plan.code === plan.code
|
||||
? "Current plan"
|
||||
: `Downgrade to ${plan.title}`}
|
||||
: `Upgrade to ${plan.title}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<ConcurrentRuns limits={plan.limits} />
|
||||
@@ -425,7 +687,7 @@ export function TierPro({
|
||||
<TierLimit href="https://trigger.dev/pricing#computePricing">
|
||||
${plan.limits.includedUsage / 100} usage included
|
||||
</TierLimit>
|
||||
<Form action={formAction} method="post" id="subscribe">
|
||||
<Form action={formAction} method="post" id="subscribe-pro">
|
||||
<div className="py-6">
|
||||
<input type="hidden" name="type" value="paid" />
|
||||
<input type="hidden" name="planCode" value={plan.code} />
|
||||
@@ -433,6 +695,7 @@ export function TierPro({
|
||||
<Button
|
||||
variant="tertiary/large"
|
||||
fullWidth
|
||||
form="subscribe-pro"
|
||||
className="text-md font-medium"
|
||||
disabled={
|
||||
isLoading ||
|
||||
@@ -589,21 +852,28 @@ function PricingHeader({
|
||||
function TierLimit({ children, href }: { children: React.ReactNode; href?: string }) {
|
||||
return (
|
||||
<>
|
||||
<hr className="my-6 border-grid-bright" />
|
||||
{href ? (
|
||||
<div>
|
||||
<hr className="my-6 border-grid-bright" />
|
||||
<a
|
||||
href={href}
|
||||
className="hover:decoration-bright font-sans text-lg font-normal text-text-bright underline decoration-charcoal-500 underline-offset-4 transition"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</div>
|
||||
<SimpleTooltip
|
||||
buttonClassName="text-left w-fit"
|
||||
disableHoverableContent
|
||||
button={
|
||||
<a
|
||||
href={href}
|
||||
className="text-left font-sans text-lg font-normal text-text-bright underline decoration-charcoal-500 underline-offset-4 transition hover:decoration-text-bright"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
}
|
||||
content={
|
||||
<div className="flex items-center gap-1">
|
||||
<Paragraph variant="small">View detailed compute pricing information</Paragraph>
|
||||
<ArrowUpRightIcon className="size-4 text-text-dimmed" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<hr className="my-6 border-grid-bright" />
|
||||
<div className="font-sans text-lg font-normal text-text-bright">{children}</div>
|
||||
</div>
|
||||
<div className="font-sans text-lg font-normal text-text-bright">{children}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user