Skip to content

Commit 3c64028

Browse files
authored
Merge pull request #621 from trycompai/main
[comp] Production Deploy
2 parents 385536c + addedd4 commit 3c64028

File tree

18 files changed

+514
-46
lines changed

18 files changed

+514
-46
lines changed

apps/app/src/actions/floating.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use server";
2+
3+
import { addYears } from "date-fns";
4+
import { createSafeActionClient } from "next-safe-action";
5+
import { cookies } from "next/headers";
6+
import { z } from "zod";
7+
8+
const schema = z.object({
9+
floatingOpen: z.boolean(),
10+
});
11+
12+
export const updateFloatingState = createSafeActionClient()
13+
.schema(schema)
14+
.action(async ({ parsedInput }) => {
15+
const cookieStore = await cookies();
16+
17+
cookieStore.set({
18+
name: "floating-onboarding-checklist",
19+
value: JSON.stringify(parsedInput.floatingOpen),
20+
expires: addYears(new Date(), 1),
21+
});
22+
23+
return { success: true };
24+
});

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export default async function Layout({
3434

3535
const cookieStore = await cookies();
3636
const isCollapsed = cookieStore.get("sidebar-collapsed")?.value === "true";
37+
const floatingOpen =
38+
cookieStore.get("floating-onboarding-checklist")?.value !== "false";
3739

3840
if (!session?.session?.userId) {
3941
return redirect("/auth");
@@ -88,12 +90,13 @@ export default async function Layout({
8890
<div className="hidden md:flex">
8991
{!("error" in onboardingStatus) &&
9092
onboardingStatus.completedItems <
91-
onboardingStatus.totalItems && (
93+
onboardingStatus.totalItems && (
9294
<FloatingOnboardingChecklist
9395
orgId={currentOrganization.id}
9496
completedItems={onboardingStatus.completedItems}
9597
totalItems={onboardingStatus.totalItems}
9698
checklistItems={onboardingStatus.checklistItems}
99+
floatingOpen={floatingOpen}
97100
/>
98101
)}
99102
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server";
2+
3+
import { authActionClient } from "@/actions/safe-action";
4+
import { db } from "@comp/db";
5+
import { z } from "zod";
6+
7+
const isFriendlyAvailableSchema = z.object({
8+
friendlyUrl: z.string(),
9+
orgId: z.string(),
10+
});
11+
12+
export const isFriendlyAvailable = authActionClient
13+
.schema(isFriendlyAvailableSchema)
14+
.metadata({
15+
name: "check-friendly-url",
16+
track: {
17+
event: "check-friendly-url",
18+
channel: "server",
19+
},
20+
})
21+
.action(async ({ parsedInput, ctx }) => {
22+
const { friendlyUrl, orgId } = parsedInput;
23+
24+
if (!ctx.session.activeOrganizationId) {
25+
throw new Error("No active organization");
26+
}
27+
28+
const url = await db.trust.findUnique({
29+
where: {
30+
friendlyUrl,
31+
},
32+
select: {
33+
friendlyUrl: true,
34+
organizationId: true,
35+
},
36+
});
37+
38+
if (url) {
39+
if (url.organizationId === orgId) {
40+
return { isAvailable: true };
41+
}
42+
return { isAvailable: false };
43+
}
44+
45+
return { isAvailable: true };
46+
});

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/trust-portal/actions/trust-portal-switch.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { z } from "zod";
1010
const trustPortalSwitchSchema = z.object({
1111
enabled: z.boolean(),
1212
contactEmail: z.string().email().optional().or(z.literal("")),
13+
friendlyUrl: z.string().optional(),
1314
});
1415

1516
export const trustPortalSwitchAction = authActionClient
@@ -22,7 +23,7 @@ export const trustPortalSwitchAction = authActionClient
2223
},
2324
})
2425
.action(async ({ parsedInput, ctx }) => {
25-
const { enabled, contactEmail } = parsedInput;
26+
const { enabled, contactEmail, friendlyUrl } = parsedInput;
2627
const { activeOrganizationId } = ctx.session;
2728

2829
if (!activeOrganizationId) {
@@ -31,15 +32,19 @@ export const trustPortalSwitchAction = authActionClient
3132

3233
try {
3334
await db.trust.upsert({
34-
where: { organizationId: activeOrganizationId },
35+
where: {
36+
organizationId: activeOrganizationId,
37+
},
3538
update: {
3639
status: enabled ? "published" : "draft",
3740
contactEmail: contactEmail === "" ? null : contactEmail,
41+
friendlyUrl: friendlyUrl === "" ? null : friendlyUrl,
3842
},
3943
create: {
4044
organizationId: activeOrganizationId,
4145
status: enabled ? "published" : "draft",
4246
contactEmail: contactEmail === "" ? null : contactEmail,
47+
friendlyUrl: friendlyUrl === "" ? null : friendlyUrl,
4348
},
4449
});
4550

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/trust-portal/components/TrustPortalSwitch.tsx

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ import { Button } from "@comp/ui/button"
1717
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@comp/ui/select"
1818
import { updateTrustPortalFrameworks } from "../actions/update-trust-portal-frameworks"
1919
import { SOC2, ISO27001, GDPR } from "./logos"
20+
import { isFriendlyAvailable } from "../actions/is-friendly-available"
21+
import { useDebounce } from "@/hooks/useDebounce"
22+
import { useState, useEffect, useRef, useCallback } from "react"
2023

2124
const trustPortalSwitchSchema = z.object({
2225
enabled: z.boolean(),
2326
contactEmail: z.string().email().or(z.literal("")).optional(),
27+
friendlyUrl: z.string().optional(),
2428
soc2: z.boolean(),
2529
iso27001: z.boolean(),
2630
gdpr: z.boolean(),
@@ -42,6 +46,7 @@ export function TrustPortalSwitch({
4246
soc2Status,
4347
iso27001Status,
4448
gdprStatus,
49+
friendlyUrl,
4550
}: {
4651
enabled: boolean
4752
slug: string
@@ -55,6 +60,7 @@ export function TrustPortalSwitch({
5560
soc2Status: "started" | "in_progress" | "compliant"
5661
iso27001Status: "started" | "in_progress" | "compliant"
5762
gdprStatus: "started" | "in_progress" | "compliant"
63+
friendlyUrl: string | null
5864
}) {
5965
const t = useI18n()
6066

@@ -67,6 +73,8 @@ export function TrustPortalSwitch({
6773
},
6874
})
6975

76+
const checkFriendlyUrl = useAction(isFriendlyAvailable)
77+
7078
const form = useForm<z.infer<typeof trustPortalSwitchSchema>>({
7179
resolver: zodResolver(trustPortalSwitchSchema),
7280
defaultValues: {
@@ -78,18 +86,113 @@ export function TrustPortalSwitch({
7886
soc2Status: soc2Status ?? "started",
7987
iso27001Status: iso27001Status ?? "started",
8088
gdprStatus: gdprStatus ?? "started",
89+
friendlyUrl: friendlyUrl ?? undefined,
8190
},
8291
})
8392

8493
const onSubmit = async (data: z.infer<typeof trustPortalSwitchSchema>) => {
8594
await trustPortalSwitch.execute(data)
8695
}
8796

88-
const portalUrl = domainVerified ? `https://${domain}` : `https://trust.trycomp.ai/${slug}`
97+
const portalUrl = domainVerified ? `https://${domain}` : `https://trust.inc/${slug}`
98+
99+
// --- Auto-save helpers ---
100+
const lastSaved = useRef<{ [key: string]: string | boolean }>({
101+
contactEmail: contactEmail ?? "",
102+
friendlyUrl: friendlyUrl ?? "",
103+
enabled: enabled,
104+
})
105+
106+
// Save handler
107+
const autoSave = useCallback(
108+
async (field: string, value: any) => {
109+
const current = form.getValues()
110+
if (lastSaved.current[field] !== value) {
111+
const data = { ...current, [field]: value }
112+
await onSubmit(data)
113+
lastSaved.current[field] = value
114+
}
115+
},
116+
[form, onSubmit]
117+
)
118+
119+
// --- Field Handlers ---
120+
// Contact Email
121+
const [contactEmailValue, setContactEmailValue] = useState(form.getValues("contactEmail") || "")
122+
const debouncedContactEmail = useDebounce(contactEmailValue, 500)
123+
// Debounced auto-save
124+
useEffect(() => {
125+
if (
126+
debouncedContactEmail !== undefined &&
127+
debouncedContactEmail !== lastSaved.current.contactEmail
128+
) {
129+
form.setValue("contactEmail", debouncedContactEmail)
130+
autoSave("contactEmail", debouncedContactEmail)
131+
}
132+
// eslint-disable-next-line react-hooks/exhaustive-deps
133+
}, [debouncedContactEmail])
134+
// On blur immediate save
135+
const handleContactEmailBlur = useCallback(
136+
(e: React.FocusEvent<HTMLInputElement>) => {
137+
const value = e.target.value
138+
form.setValue("contactEmail", value)
139+
autoSave("contactEmail", value)
140+
},
141+
[form, autoSave]
142+
)
143+
144+
// Friendly URL
145+
const [friendlyUrlValue, setFriendlyUrlValue] = useState(form.getValues("friendlyUrl") || "")
146+
const debouncedFriendlyUrl = useDebounce(friendlyUrlValue, 500)
147+
const [friendlyUrlStatus, setFriendlyUrlStatus] = useState<"idle" | "checking" | "available" | "unavailable">("idle")
148+
// Check availability on debounce
149+
useEffect(() => {
150+
if (!debouncedFriendlyUrl || debouncedFriendlyUrl === (friendlyUrl ?? "")) {
151+
setFriendlyUrlStatus("idle")
152+
return
153+
}
154+
setFriendlyUrlStatus("checking")
155+
checkFriendlyUrl.execute({ friendlyUrl: debouncedFriendlyUrl, orgId })
156+
// eslint-disable-next-line react-hooks/exhaustive-deps
157+
}, [debouncedFriendlyUrl, orgId, friendlyUrl])
158+
useEffect(() => {
159+
if (checkFriendlyUrl.status === "executing") return
160+
if (checkFriendlyUrl.result?.data?.isAvailable === true) {
161+
setFriendlyUrlStatus("available")
162+
// Auto-save if available and changed
163+
if (debouncedFriendlyUrl !== lastSaved.current.friendlyUrl) {
164+
form.setValue("friendlyUrl", debouncedFriendlyUrl)
165+
autoSave("friendlyUrl", debouncedFriendlyUrl)
166+
}
167+
} else if (checkFriendlyUrl.result?.data?.isAvailable === false) {
168+
setFriendlyUrlStatus("unavailable")
169+
}
170+
// eslint-disable-next-line react-hooks/exhaustive-deps
171+
}, [checkFriendlyUrl.status, checkFriendlyUrl.result])
172+
// On blur immediate save if available
173+
const handleFriendlyUrlBlur = useCallback(
174+
(e: React.FocusEvent<HTMLInputElement>) => {
175+
const value = e.target.value
176+
if (friendlyUrlStatus === "available" && value !== lastSaved.current.friendlyUrl) {
177+
form.setValue("friendlyUrl", value)
178+
autoSave("friendlyUrl", value)
179+
}
180+
},
181+
[form, autoSave, friendlyUrlStatus]
182+
)
183+
184+
// Enabled switch immediate save
185+
const handleEnabledChange = useCallback(
186+
(val: boolean) => {
187+
form.setValue("enabled", val)
188+
autoSave("enabled", val)
189+
},
190+
[form, autoSave]
191+
)
89192

90193
return (
91194
<Form {...form}>
92-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
195+
<form className="space-y-4">
93196
<Card className="overflow-hidden">
94197
<CardHeader className="pb-4">
95198
<div className="flex items-center justify-between">
@@ -114,7 +217,7 @@ export function TrustPortalSwitch({
114217
<FormControl>
115218
<Switch
116219
checked={field.value}
117-
onCheckedChange={field.onChange}
220+
onCheckedChange={handleEnabledChange}
118221
disabled={trustPortalSwitch.status === "executing"}
119222
/>
120223
</FormControl>
@@ -126,19 +229,64 @@ export function TrustPortalSwitch({
126229
<CardContent className="space-y-6 pt-0">
127230
{form.watch("enabled") && (
128231
<div className="pt-2">
129-
<h3 className="text-sm font-medium mb-4">Information Requests</h3>
130-
<div className="rounded-md border p-4">
232+
<h3 className="text-sm font-medium mb-4">Trust Portal Settings</h3>
233+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-6 border rounded-md p-4">
234+
<FormField
235+
control={form.control}
236+
name="friendlyUrl"
237+
render={({ field }) => (
238+
<FormItem className="w-full">
239+
<FormLabel>
240+
Custom URL
241+
</FormLabel>
242+
<FormControl>
243+
<div>
244+
<div className="relative flex items-center w-full">
245+
<Input
246+
{...field}
247+
value={friendlyUrlValue}
248+
onChange={e => {
249+
field.onChange(e)
250+
setFriendlyUrlValue(e.target.value)
251+
}}
252+
onBlur={handleFriendlyUrlBlur}
253+
placeholder="my-org"
254+
autoComplete="off"
255+
autoCapitalize="none"
256+
autoCorrect="off"
257+
spellCheck="false"
258+
prefix="trust.inc/"
259+
/>
260+
</div>
261+
{friendlyUrlValue && (
262+
<div className="text-xs mt-1 min-h-[18px]">
263+
{friendlyUrlStatus === "checking" && t("settings.trust_portal.friendly_url.checking")}
264+
{friendlyUrlStatus === "available" && <span className="text-green-600">{t("settings.trust_portal.friendly_url.available")}</span>}
265+
{friendlyUrlStatus === "unavailable" && <span className="text-red-600">{t("settings.trust_portal.friendly_url.unavailable")}</span>}
266+
</div>
267+
)}
268+
</div>
269+
</FormControl>
270+
</FormItem>
271+
)}
272+
/>
131273
<FormField
132274
control={form.control}
133275
name="contactEmail"
134276
render={({ field }) => (
135-
<FormItem className="flex items-center justify-between">
277+
<FormItem className="w-full">
136278
<FormLabel>
137279
Contact Email
138280
</FormLabel>
139281
<FormControl>
140282
<Input
141283
{...field}
284+
value={contactEmailValue}
285+
onChange={e => {
286+
field.onChange(e)
287+
setContactEmailValue(e.target.value)
288+
}}
289+
onBlur={handleContactEmailBlur}
142290
placeholder="[email protected]"
143291
className="w-auto"
144292
autoComplete="off"
@@ -259,10 +407,6 @@ export function TrustPortalSwitch({
259407
) : (
260408
<p className="text-xs text-muted-foreground">Trust portal is currently disabled.</p>
261409
)}
262-
<Button type="submit" disabled={trustPortalSwitch.status === "executing"}>
263-
{trustPortalSwitch.status === "executing" ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
264-
{t("common.actions.save")}
265-
</Button>
266410
</CardFooter>
267411
</Card>
268412
</form>

0 commit comments

Comments
 (0)