Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 37 additions & 54 deletions apps/builder/app/builder/features/topbar/add-domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,65 +7,61 @@ import {
theme,
Text,
Grid,
toast,
} from "@webstudio-is/design-system";
import { validateDomain } from "@webstudio-is/domain";
import type { Project } from "@webstudio-is/project";
import { useId, useState } from "react";
import { useId, useOptimistic, useRef, useState } from "react";
import { CustomCodeIcon } from "@webstudio-is/icons";
import { trpcClient } from "~/shared/trpc/trpc-client";
import { nativeClient } from "~/shared/trpc/trpc-client";

type DomainsAddProps = {
projectId: Project["id"];
onCreate: (domain: string) => void;
onExportClick: () => void;
refreshDomainResult: (
input: { projectId: Project["id"] },
onSuccess: () => void
) => void;
domainState: "idle" | "submitting";
isPublishing: boolean;
refresh: () => Promise<void>;
};

export const AddDomain = ({
projectId,
onCreate,
refreshDomainResult,
domainState,
isPublishing,
refresh,
onExportClick,
}: DomainsAddProps) => {
const id = useId();
const {
send: create,
state: сreateState,
error: сreateSystemError,
} = trpcClient.domain.create.useMutation();
const [isOpen, setIsOpen] = useState(false);
const [domain, setDomain] = useState("");
const [error, setError] = useState<string>();
const buttonRef = useRef<HTMLButtonElement>(null);
const [isPending, setIsPendingOptimistic] = useOptimistic(false);

const handleCreate = () => {
setError(undefined);
const handleCreateDomain = async (formData: FormData) => {
// Will be automatically reset on action end
setIsPendingOptimistic(true);

const domain = formData.get("domain")?.toString() ?? "";
const validationResult = validateDomain(domain);

if (validationResult.success === false) {
setError(validationResult.error);
return;
}

create({ domain: validationResult.domain, projectId }, (data) => {
if (data.success === false) {
setError(data.error);
return;
}

refreshDomainResult({ projectId }, () => {
setDomain("");
setIsOpen(false);
onCreate(validationResult.domain);
});
const result = await nativeClient.domain.create.mutate({
domain,
projectId,
});

if (result.success === false) {
toast.error(result.error);
setError(result.error);
return;
}

onCreate(domain);

await refresh();

setIsOpen(false);
};

return (
Expand All @@ -81,7 +77,6 @@ export const AddDomain = ({
direction={"column"}
onKeyDown={(event) => {
if (event.key === "Escape") {
setDomain("");
setIsOpen(false);
event.preventDefault();
}
Expand All @@ -94,56 +89,43 @@ export const AddDomain = ({
</Label>
<InputField
id={id}
name="domain"
autoFocus
placeholder="your-domain.com"
value={domain}
disabled={
isPublishing || сreateState !== "idle" || domainState !== "idle"
}
disabled={isPending}
onKeyDown={(event) => {
if (event.key === "Enter") {
handleCreate();
buttonRef.current
?.closest("form")
?.requestSubmit(buttonRef.current);
}
if (event.key === "Escape") {
setDomain("");
setIsOpen(false);
event.preventDefault();
}
}}
onChange={(event) => {
setError(undefined);
setDomain(event.target.value);
}}
color={error !== undefined ? "error" : undefined}
/>
{error !== undefined && (
<>
<Text color="destructive">{error}</Text>
</>
)}
{сreateSystemError !== undefined && (
<>
{/* Something happened with network, api etc */}
<Text color="destructive">{сreateSystemError}</Text>
<Text color="subtle">Please try again later</Text>
</>
)}
</>
)}

<Grid gap={2} columns={2}>
<Button
disabled={
isPublishing || сreateState !== "idle" || domainState !== "idle"
}
ref={buttonRef}
formAction={handleCreateDomain}
state={isPending ? "pending" : undefined}
color={isOpen ? "primary" : "neutral"}
onClick={() => {
onClick={(event) => {
if (isOpen === false) {
setIsOpen(true);
event.preventDefault();
return;
}

handleCreate();
}}
>
{isOpen ? "Add domain" : "Add a new domain"}
Expand All @@ -152,6 +134,7 @@ export const AddDomain = ({
<Button
color={"dark"}
prefix={<CustomCodeIcon />}
type="button"
onClick={onExportClick}
>
Export
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Label,
theme,
SectionTitle,
Grid,
} from "@webstudio-is/design-system";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
Expand All @@ -13,9 +14,11 @@ export const CollapsibleDomainSection = ({
children,
title,
suffix,
prefix,
}: {
initiallyOpen?: boolean;
children: ReactNode;
prefix: ReactNode;
suffix: ReactNode;
title: string;
}) => {
Expand Down Expand Up @@ -53,7 +56,10 @@ export const CollapsibleDomainSection = ({
</Box>
}
>
<Label truncate>{title}</Label>
<Grid flow={"column"} align="center" justify={"start"}>
{prefix}
<Label truncate>{title}</Label>
</Grid>
</SectionTitle>
}
>
Expand Down
82 changes: 82 additions & 0 deletions apps/builder/app/builder/features/topbar/domain-checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useStore } from "@nanostores/react";
import {
Flex,
Tooltip,
Text,
theme,
Link,
buttonStyle,
Checkbox,
} from "@webstudio-is/design-system";
import { $userPlanFeatures } from "~/builder/shared/nano-states";
import { $project } from "~/shared/nano-states";

export const domainToPublishName = "domainToPublish[]";

interface DomainCheckboxProps {
defaultChecked?: boolean;
domain: string;
buildId: string | undefined;
disabled?: boolean;
}

const DomainCheckbox = (props: DomainCheckboxProps) => {
const hasProPlan = useStore($userPlanFeatures).hasProPlan;
const project = useStore($project);

if (project === undefined) {
return;
}

const tooltipContentForFreeUsers = hasProPlan ? undefined : (
<Flex direction="column" gap="2" css={{ maxWidth: theme.spacing[28] }}>
<Text variant="titles">Publish to Staging</Text>
<Text>
<Flex direction="column">
Staging allows you to preview a production version of your site
without potentially breaking what production site visitors will see.
<>
<br />
<br />
Upgrade to Pro account to publish to each domain individually.
<br /> <br />
<Link
className={buttonStyle({ color: "gradient" })}
color="contrast"
underline="none"
href="https://webstudio.is/pricing"
target="_blank"
>
Upgrade
</Link>
</>
</Flex>
</Text>
</Flex>
);

const defaultChecked = hasProPlan ? props.defaultChecked : true;
const disabled = hasProPlan ? props.disabled : true;

const hideDomainCheckbox =
project.domainsVirtual.filter(
(domain) => domain.status === "ACTIVE" && domain.verified
).length === 0 && hasProPlan;

return (
<div style={{ display: hideDomainCheckbox ? "none" : "contents" }}>
<Tooltip content={tooltipContentForFreeUsers} variant="wrapped">
<Checkbox
disabled={disabled}
key={props.buildId ?? "-"}
defaultChecked={hideDomainCheckbox || defaultChecked}
css={{ pointerEvents: "all" }}
name={domainToPublishName}
value={props.domain}
/>
</Tooltip>
</div>
);
};

export default DomainCheckbox;
Loading