feat: set workspace url when creating a new workspace (#172)

* feat: add migration to remove not null constraint from referenceId on subscriptions

* feat(cloud): write worspace slug availability checks to db

* feat: add migration to add cascade set null to referenceId on subscription

* feat: update workspace and subscripton schema

* feat: add createWorkspaceSlugCheck repo func

* feat: set workspace url on creation

* chore: update translations

* refactor: various tweaks and fixes

* feat: add pro bolt to pricing notice

* feat: toggle pro subscription

* fix: hide pro subscription notice for self hosters

* feat: add pro upgrade message on members page

* feat: hide pro upgrade notice on small viewports

* feat: update workspace url placeholder
This commit is contained in:
Henry
2025-09-11 20:32:02 +01:00
committed by GitHub
parent 4f7fa1a228
commit 2f88366418
31 changed files with 8942 additions and 385 deletions

View File

@@ -14,9 +14,9 @@ import { NewBoardForm } from "./components/NewBoardForm";
export default function BoardsPage() {
const { openModal, modalContentType, isOpen } = useModal();
const { workspace, hasLoaded } = useWorkspace();
const { availableWorkspaces, workspace, hasLoaded } = useWorkspace();
if (hasLoaded && !workspace.publicId) openModal("NEW_WORKSPACE");
if (hasLoaded && availableWorkspaces.length === 0) openModal("NEW_WORKSPACE");
return (
<>

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { Radio, RadioGroup } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { useState } from "react";
import { HiCheckCircle } from "react-icons/hi2";
import { HiBolt, HiCheckCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
type Frequency = "monthly" | "annually";
@@ -94,6 +94,7 @@ const Pricing = () => {
<div className="mt-12 flex flex-col items-center justify-center">
<div className="mb-4 flex items-center gap-2 rounded-full border bg-white px-4 py-1.5 text-center text-xs font-bold text-gray-800 dark:border-dark-300 dark:bg-white dark:text-gray-800 lg:text-sm">
<HiBolt />
<p>
{t`Launch offer: unlimited seats for just $29/month with Pro`}
</p>

View File

@@ -1,6 +1,11 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { HiEllipsisHorizontal, HiOutlinePlusSmall } from "react-icons/hi2";
import {
HiBolt,
HiEllipsisHorizontal,
HiOutlinePlusSmall,
} from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
import type { Subscription } from "@kan/shared/utils";
@@ -168,30 +173,45 @@ export default function MembersPage() {
<PageHead title={t`Members | ${workspace.name ?? "Workspace"}`} />
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
<div className="mb-8 flex w-full justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`Members`}
</h1>
<div className="flex items-center gap-3">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`Members`}
</h1>
</div>
<div className="flex items-center gap-3">
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
<div
className={twMerge(
"flex items-center rounded-full border px-3 py-1 text-center text-xs",
teamSubscription || proSubscription
? "border-emerald-300 bg-emerald-50 text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
: "border-light-300 bg-light-50 text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900",
<>
{!proSubscription && (
<Link
href="/settings?upgrade=pro"
className="hidden items-center rounded-full border border-emerald-300 bg-emerald-50 px-3 py-1 text-center text-xs text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400 lg:flex"
>
<HiBolt />
<span className="ml-1 font-medium">
{t`Launch offer: Get unlimited members with Pro`}
</span>
</Link>
)}
>
<span className="font-medium">
{proSubscription
? t`Pro Plan`
: teamSubscription
? t`Team Plan`
: t`Free Plan`}
{proSubscription && unlimitedSeats && (
<span className="ml-1 text-xs"></span>
<div
className={twMerge(
"flex items-center rounded-full border px-3 py-1 text-center text-xs",
teamSubscription || proSubscription
? "border-emerald-300 bg-emerald-50 text-emerald-400 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
: "border-light-300 bg-light-50 text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900",
)}
</span>
</div>
>
<span className="font-medium">
{proSubscription
? t`Pro Plan`
: teamSubscription
? t`Team Plan`
: t`Free Plan`}
{proSubscription && unlimitedSeats && (
<span className="ml-1 text-xs"></span>
)}
</span>
</div>
</>
)}
<Button
onClick={() => openModal("INVITE_MEMBER")}

View File

@@ -76,9 +76,14 @@ export function UpgradeToProConfirmation({
</div>
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-5 w-5 flex-shrink-0 text-light-1000 dark:text-dark-950" />
<span className="text-sm text-neutral-900 dark:text-dark-1000">
{t`Board analytics (coming soon)`}
</span>
<div className="flex items-center space-x-2">
<span className="text-xs text-neutral-900 dark:text-dark-1000">
{t`Board analytics`}
</span>
<span className="inline-flex items-center rounded-full bg-gray-500/10 px-2 py-0.5 text-[10px] font-medium text-gray-600 ring-1 ring-inset ring-gray-500/20 dark:text-gray-400 sm:text-[10px]">
{t`Coming soon`}
</span>
</div>
</div>
<div className="flex items-center space-x-3">
<HiCheckBadge className="h-5 w-5 flex-shrink-0 text-light-1000 dark:text-dark-950" />
@@ -90,7 +95,12 @@ export function UpgradeToProConfirmation({
</div>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button onClick={() => closeModal()} variant="secondary">
<Button
onClick={() => {
closeModal();
}}
variant="secondary"
>
{t`Cancel`}
</Button>
<Button

View File

@@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { HiBolt, HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
import type { Subscription } from "@kan/shared/utils";
@@ -36,6 +36,7 @@ export default function SettingsPage() {
const router = useRouter();
const workspaceUrlSectionRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
const isCredentialsEnabled =
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
const { data } = api.user.getUser.useQuery();
@@ -100,11 +101,13 @@ export default function SettingsPage() {
if (
router.query.upgrade === "pro" &&
env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
!hasActiveSubscription(subscriptions, "pro")
!hasActiveSubscription(subscriptions, "pro") &&
!hasOpenedUpgradeModal
) {
openModal("UPGRADE_TO_PRO");
setHasOpenedUpgradeModal(true);
}
}, [router.query.upgrade, subscriptions, openModal]);
}, [router.query.upgrade, subscriptions, openModal, hasOpenedUpgradeModal]);
const { mutateAsync: disconnectTrello } =
api.integration.disconnect.useMutation({