Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f0c14b9
feat: update OrganizationSetupForm and routing logic
claudfuen Jun 24, 2025
a6777b2
chore: remove 'Other' work location option
claudfuen Jun 24, 2025
7062eb3
feat: enhance onboarding step input with website field
claudfuen Jun 24, 2025
c14fca1
chore(deps): bump @react-email/tailwind from 1.0.4 to 1.0.5
dependabot[bot] Jun 24, 2025
0607f59
chore(deps): bump react-hotkeys-hook from 4.6.2 to 5.1.0
dependabot[bot] Jun 24, 2025
a9ddb3b
refactor: update layout components to use dynamic viewport height
claudfuen Jun 24, 2025
2da9632
chore(deps): bump @tiptap/extension-text-align from 2.14.0 to 2.22.3
dependabot[bot] Jun 24, 2025
6243784
refactor: update styling for Select and Textarea components
claudfuen Jun 24, 2025
0079556
feat: enhance OnboardingStepInput and SelectPills components
claudfuen Jun 24, 2025
b145e16
Merge pull request #1024 from trycompai/codex/remove-other-option-fro…
claudfuen Jun 24, 2025
edeb0a4
Merge branch 'main' of https://github.com/trycompai/comp into claudio…
claudfuen Jun 24, 2025
0599fcc
feat: add animated pricing banner to upgrade page
claudfuen Jun 24, 2025
f70f8eb
style: update AnimatedPricingBanner for improved sticky behavior
claudfuen Jun 24, 2025
a8c6ff1
feat: enhance upgrade page with logo marquee and review widget
claudfuen Jun 24, 2025
ae3464a
refactor: streamline logo components and update styling
claudfuen Jun 24, 2025
24ecd81
feat: integrate Senja Review Widget into pricing cards
claudfuen Jun 24, 2025
8d630d8
refactor: replace SenjaReviewWidget with ReviewSection in pricing cards
claudfuen Jun 24, 2025
41c4630
Merge pull request #1023 from trycompai/claudio/comp-225-clean-up-ani…
claudfuen Jun 24, 2025
f126a7e
Merge pull request #1007 from trycompai/dependabot/npm_and_yarn/main/…
claudfuen Jun 24, 2025
dc2c7b8
Merge pull request #994 from trycompai/dependabot/npm_and_yarn/main/r…
claudfuen Jun 24, 2025
2489ce9
Merge pull request #993 from trycompai/dependabot/npm_and_yarn/main/r…
claudfuen Jun 24, 2025
5a95303
chore(deps): update dependencies for react-hotkeys-hook and @tiptap e…
claudfuen Jun 24, 2025
ec6fbb3
Merge pull request #1028 from trycompai/claudio/comp-225-clean-up-ani…
claudfuen Jun 24, 2025
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
2 changes: 1 addition & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"puppeteer-core": "^24.7.2",
"react-email": "^4.0.15",
"react-hook-form": "^7.58.1",
"react-hotkeys-hook": "^4.6.2",
"react-hotkeys-hook": "^5.1.0",
"react-intersection-observer": "^9.16.0",
"react-markdown": "^9.1.0",
"react-textarea-autosize": "^8.5.9",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/app/(app)/no-access/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default async function NoAccess() {
});

return (
<div className="bg-foreground/05 flex h-screen flex-col items-center justify-center gap-4">
<div className="bg-foreground/05 flex h-dvh flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Access Denied</h1>
<div className="flex flex-col text-center">
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ function AnimatedOrb({ scale = 1 }: AnimatedOrbProps) {
// Handle ESC key for rage mode easter egg
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't process keyboard events if user is typing in an input
const target = e.target as HTMLElement;
const isInputElement = target.matches('input, textarea, select, [contenteditable="true"]');

if (isInputElement) {
return; // Skip processing for form inputs
}

if (e.key === 'Escape') {
e.preventDefault();
rageMode.current = !rageMode.current;
Expand Down
14 changes: 13 additions & 1 deletion apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { SelectPills } from '@comp/ui/select-pills';
import { Textarea } from '@comp/ui/textarea';
import type { UseFormReturn } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { CompanyDetails, Step } from '../lib/types';
import { FrameworkSelection } from './FrameworkSelection';
import { WebsiteInput } from './WebsiteInput';

// Type for form fields used in this component.
// For now, defining it here to match OrganizationSetupForm.tsx structure.
Expand Down Expand Up @@ -35,6 +37,16 @@ export function OnboardingStepInput({
);
}

if (currentStep.key === 'website') {
return (
<Controller
name={currentStep.key}
control={form.control}
render={({ field }) => <WebsiteInput {...field} placeholder="example.com" autoFocus />}
/>
);
}

if (currentStep.key === 'describe') {
return (
<Textarea
Expand Down Expand Up @@ -81,7 +93,7 @@ export function OnboardingStepInput({
onValueChange={(values: string[]) => {
form.setValue(currentStep.key, values.join(','));
}}
placeholder={`Type anything and press enter to add it, ${currentStep.placeholder}`}
placeholder={`Search or add custom (press Enter) • ${currentStep.placeholder}`}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function OrganizationSetupForm({
}, [stepIndex, steps.length, isFinalizing]);

return isFinalizing ? (
<div className="flex min-h-screen items-center justify-center">
<div className="flex min-h-dvh items-center justify-center">
<LogoSpinner />
</div>
) : (
Expand Down Expand Up @@ -157,9 +157,9 @@ export function OrganizationSetupForm({
</div>
<div className="w-full border-t border-border/30 pt-3">
<p className="text-center text-xs text-muted-foreground/70">
<span className="inline-flex items-center gap-1">
<span className="inline-flex items-center justify-center gap-1.5 flex-wrap">
<svg
className="h-3 w-3"
className="h-3.5 w-3.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
Expand All @@ -168,11 +168,13 @@ export function OrganizationSetupForm({
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
strokeWidth={1.5}
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
Your answers will be used by our AI to create a personalized compliance plan
<span className="max-w-[280px] sm:max-w-none">
AI personalizes your plan based on your answers
</span>
</span>
</p>
</div>
Expand Down
114 changes: 114 additions & 0 deletions apps/app/src/app/(app)/setup/components/WebsiteInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client';

import { Input } from '@comp/ui/input';
import { type InputHTMLAttributes, forwardRef, useCallback, useEffect, useState } from 'react';

interface WebsiteInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'prefix'> {
onValueChange?: (value: string) => void;
}

export const WebsiteInput = forwardRef<HTMLInputElement, WebsiteInputProps>(
({ value: propValue, onChange, onValueChange, onBlur, ...props }, ref) => {
// Use local state for the display value
const [displayValue, setDisplayValue] = useState('');

// Update display value when prop value changes
useEffect(() => {
if (typeof propValue === 'string') {
setDisplayValue(propValue.replace(/^https?:\/\//, '').replace(/^www\./, ''));
}
}, [propValue]);

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
let inputValue = e.target.value;

// Clean up the input
inputValue = inputValue.trim();

// Remove any protocol if pasted
inputValue = inputValue.replace(/^https?:\/\//, '');
inputValue = inputValue.replace(/^ftp:\/\//, '');
inputValue = inputValue.replace(/^\/\//, '');

// Clean up multiple slashes (except for the protocol)
inputValue = inputValue.replace(/([^:]\/)\/+/g, '$1');

// Update display value
setDisplayValue(inputValue);

// If empty, pass empty value
if (!inputValue) {
onChange?.({
...e,
target: { ...e.target, value: '', name: e.target.name },
} as React.ChangeEvent<HTMLInputElement>);
onValueChange?.('');
return;
}

// Format the value with https://
const finalValue = `https://${inputValue}`;

// Create a synthetic event with the formatted value
const syntheticEvent = {
...e,
target: {
...e.target,
value: finalValue,
name: e.target.name,
},
} as React.ChangeEvent<HTMLInputElement>;

// Call the original onChange if provided
onChange?.(syntheticEvent);

// Also call onValueChange if provided
onValueChange?.(finalValue);
},
[onChange, onValueChange],
);

const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
// On blur, ensure the value has a proper format
if (displayValue && !displayValue.includes('.')) {
// If there's no dot, add .com as a helpful default
const finalValue = `https://${displayValue}.com`;
const syntheticEvent = {
...e,
target: {
...e.target,
value: finalValue,
name: e.target.name,
},
} as React.FocusEvent<HTMLInputElement>;

setDisplayValue(`${displayValue}.com`);
onChange?.({ ...syntheticEvent, type: 'change' } as React.ChangeEvent<HTMLInputElement>);
onValueChange?.(finalValue);
}

// Call original onBlur
onBlur?.(e);
},
[displayValue, onChange, onValueChange, onBlur],
);

return (
<Input
ref={ref}
type="text"
value={displayValue}
onChange={handleChange}
onBlur={handleBlur}
prefix="https://"
autoComplete="url"
spellCheck={false}
{...props}
/>
);
},
);

WebsiteInput.displayName = 'WebsiteInput';
2 changes: 1 addition & 1 deletion apps/app/src/app/(app)/setup/go/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function RunPage({ params }: PageProps) {

return (
<TriggerProvider accessToken={publicAccessToken?.value ?? ''}>
<div className="bg-background flex min-h-screen items-center justify-center p-6 md:p-8">
<div className="bg-background flex min-h-dvh items-center justify-center p-6 md:p-8">
<div className="bg-card relative w-full max-w-[440px] border p-8 shadow-lg">
<div className="animate-in fade-in slide-in-from-bottom-4 flex flex-col justify-center space-y-4 duration-300">
<div className="flex flex-col justify-center gap-2">
Expand Down
8 changes: 4 additions & 4 deletions apps/app/src/app/(app)/setup/hooks/useOnboardingForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export function useOnboardingForm({
setIsFinalizing(true);
sendGTMEvent({ event: 'conversion' });

// Organization created, now redirect to loading step
router.push(`/setup/loading/${data.organizationId}`);
// Organization created, now redirect to plans page
router.push(`/upgrade/${data.organizationId}`);
} else {
toast.error(data?.error || 'Failed to create organization minimal');
setIsSkipping(false);
Expand All @@ -105,8 +105,8 @@ export function useOnboardingForm({
setIsFinalizing(true);
sendGTMEvent({ event: 'conversion' });

// Organization created, now redirect to loading step
router.push(`/setup/loading/${data.organizationId}`);
// Organization created, now redirect to plans page
router.push(`/upgrade/${data.organizationId}`);

setSavedAnswers({});
} else {
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/app/(app)/setup/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default async function SetupLayout({ children }: { children: React.ReactN
const currentOrganization = null; // No current org in setup

return (
<main className="flex min-h-screen flex-col">
<main className="flex min-h-dvh flex-col">
<AnimatedGradientBackgroundWrapper />
<MinimalHeader
user={session.user}
Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/app/(app)/setup/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const steps: Step[] = [
{
key: 'website',
question: "What's your company website?",
placeholder: 'e.g., https://www.acme.com',
placeholder: 'example.com',
},
{
key: 'describe',
Expand Down Expand Up @@ -100,7 +100,7 @@ export const steps: Step[] = [
key: 'workLocation',
question: 'How does your team work?',
placeholder: 'e.g., Remote',
options: ['Fully remote', 'Hybrid (office + remote)', 'Office-based', 'Other'],
options: ['Fully remote', 'Hybrid (office + remote)', 'Office-based'],
},
{
key: 'infrastructure',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

import { AnimatedGradientBackground } from '@/app/(app)/setup/components/AnimatedGradientBackground';
import { Sparkles } from 'lucide-react';
import { useEffect, useState } from 'react';

export function AnimatedPricingBanner() {
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

const messages = [
'AI is analyzing your compliance needs',
'Customizing your security framework',
'Building your compliance roadmap',
'Optimizing for your industry requirements',
];

const [currentMessageIndex, setCurrentMessageIndex] = useState(0);

useEffect(() => {
if (!mounted) return;

const interval = setInterval(() => {
setCurrentMessageIndex((prev) => (prev + 1) % messages.length);
}, 3000);

return () => clearInterval(interval);
}, [mounted, messages.length]);

if (!mounted) return null;

return (
<div className="sticky top-[49px] z-[9] w-full h-10 overflow-hidden select-none">
{/* Background with gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 via-primary/5 to-primary/10 backdrop-blur-md" />

{/* Clipped animated background */}
<div className="absolute inset-0 overflow-hidden opacity-60">
<div className="absolute inset-0 scale-[3] translate-y-1/2">
<AnimatedGradientBackground scale={2} />
</div>
</div>

{/* Shimmer effect overlay */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute inset-0 -translate-x-full animate-[shimmer_3s_ease-in-out_infinite] bg-gradient-to-r from-transparent via-primary/10 to-transparent" />
</div>

{/* Top border for depth when sticky */}
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/20 to-transparent opacity-0 transition-opacity duration-300 group-[.scrolled]:opacity-100" />

{/* Bottom border with glow */}
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/50 to-transparent" />

{/* Content */}
<div className="relative h-full flex items-center justify-center px-4">
<div className="flex items-center gap-3">
<div className="relative">
<Sparkles className="h-4 w-4 text-primary" />
<div className="absolute inset-0 blur-sm bg-primary/50 animate-pulse" />
</div>

<span className="text-sm font-medium text-foreground/90 transition-all duration-500 ease-in-out">
{messages[currentMessageIndex]}
</span>

<div className="flex gap-1 ml-1">
<span className="w-1.5 h-1.5 bg-primary/70 rounded-full animate-pulse" />
<span className="w-1.5 h-1.5 bg-primary/70 rounded-full animate-pulse [animation-delay:150ms]" />
<span className="w-1.5 h-1.5 bg-primary/70 rounded-full animate-pulse [animation-delay:300ms]" />
</div>
</div>
</div>

<style jsx>{`
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}
`}</style>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';

const SVGComponent = (props: React.SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 240 31.775"
fill="none"
className="h-10 w-30 grayscale opacity-70 text-muted-foreground dark:text-white"
width={120}
height={40}
{...props}
>
<path
d="M0.135 31.099V0.811h6.057V31.099zm11.26 0V0.811h4.586l14.754 17.523c0.692 0.779 1.385 1.601 1.99 2.51 -0.173 -1.298 -0.216 -3.159 -0.216 -4.933V0.811h5.538V31.099h-3.937L18.794 12.883c-0.692 -0.779 -1.385 -1.601 -1.99 -2.51 0.173 1.298 0.216 3.159 0.216 4.933V31.099zm31.838 0V0.811h10.211c6.576 0 10.124 3.245 10.124 7.961 0 2.899 -1.168 4.976 -4.889 6.36 4.932 1.168 6.576 3.851 6.576 7.615C65.256 27.81 61.102 31.099 54.526 31.099zm6.057 -5.452h4.673c3.288 0 5.149 -1.211 5.149 -3.851 0 -2.207 -1.861 -3.548 -5.15 -3.548h-4.673zm0 -12.331h3.591c3.289 0 4.543 -1.168 4.543 -3.591 0 -2.077 -1.254 -3.505 -4.543 -3.505h-3.591zm33.941 18.345c-9 0 -15.793 -7.009 -15.793 -15.663C67.439 7.301 74.231 0.249 83.231 0.249c8.956 0 15.706 7.053 15.706 15.749 0 8.654 -6.75 15.663 -15.706 15.663m-9.519 -15.663c0 5.495 3.634 10.038 9.519 10.038 5.841 0 9.432 -4.543 9.432 -10.038s-3.591 -10.125 -9.432 -10.125c-5.884 0 -9.519 4.63 -9.519 10.125M98.31 31.099l9.952 -15.187L98.786 0.811h7.226l5.971 10.038L118.083 0.811h7.053l-9.476 15.23L125.612 31.099h-7.312l-6.317 -10.081 -6.49 10.081zm36.019 0v-0.649l13.759 -22.932a13.521 13.521 0 0 1 0.995 -1.385h-13.932V0.811h23.581v0.649l-13.759 22.932a13.521 13.521 0 0 1 -0.995 1.385h14.754V31.099zm28.033 0V0.811h17.74v5.452h-11.682v6.793h11.466v5.279h-11.466v7.312h11.769V31.099zm22.541 0V0.811l9.778 -0.043c6.793 -0.043 10.947 3.288 10.947 9.216 0 4.803 -2.683 7.745 -7.269 8.74L206.839 31.099h-7.139l-6.447 -9.605c-0.433 -0.649 -0.779 -1.298 -1.168 -2.207h-1.125V31.099zm6.057 -17.264h3.375c3.288 0 5.149 -1.341 5.149 -3.808 0 -2.423 -1.861 -3.764 -5.149 -3.764h-3.375zm32.843 17.826c-9 0 -15.793 -7.009 -15.793 -15.663 0 -8.697 6.793 -15.749 15.793 -15.749 8.956 0 15.706 7.053 15.706 15.749 0 8.654 -6.75 15.663 -15.706 15.663m-9.519 -15.663c0 5.495 3.634 10.038 9.519 10.038 5.841 0 9.432 -4.543 9.432 -10.038s-3.591 -10.125 -9.432 -10.125c-5.884 0 -9.519 4.63 -9.519 10.125"
fill="currentColor"
/>
</svg>
);
export default SVGComponent;
Loading
Loading