-
Notifications
You must be signed in to change notification settings - Fork 477
Vercel integration app redesign #1015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: project-settings-redesign
Are you sure you want to change the base?
Vercel integration app redesign #1015
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile OverviewGreptile SummaryThis PR redesigns the Vercel integration page with a modern, interactive UI featuring collapsible step cards, animated progress tracking, and a celebratory confetti effect upon completion. Key changes:
Confidence Score: 5/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant User
participant PageClient
participant State
participant AdminApp
participant Confetti
User->>PageClient: Load page
PageClient->>State: Initialize state (keys, expandedStep, etc.)
PageClient->>State: Calculate progress & nextItem
alt First load or next step changes
PageClient->>State: Auto-expand next step section
end
PageClient->>PageClient: Animate progress bar (100ms delay)
User->>PageClient: Click "Generate keys"
PageClient->>AdminApp: createInternalApiKey()
AdminApp-->>PageClient: Return keys
PageClient->>State: setKeys(newKeys)
PageClient->>State: Recalculate progress
User->>PageClient: Click step header
PageClient->>State: Toggle expandedStepId
User->>PageClient: Click checklist item
PageClient->>State: toggleStepCompletion()
State->>State: Update manuallyCompleted (cascading logic)
PageClient->>State: Recalculate progress
alt All steps completed
PageClient->>Confetti: Trigger confetti animation
loop Every 250ms for 3 seconds
Confetti->>Confetti: Fire confetti particles
end
end
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 file reviewed, no comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR redesigns the Vercel integration page with an improved UI/UX featuring expandable/collapsible step cards, animated progress tracking, and celebratory confetti effects upon completion.
Key Changes
- Redesigned step cards with expand/collapse functionality and improved visual hierarchy
- Added animated progress bar with real-time completion tracking
- Introduced confetti celebration effect when all integration steps are completed
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const STATUS_META: Record< | ||
| StepStatus, | ||
| { | ||
| cardClass: string, | ||
| inactiveIcon: string, | ||
| } | ||
| > = { | ||
| done: { | ||
| cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5", | ||
| inactiveIcon: "text-emerald-500 dark:text-emerald-400", | ||
| }, | ||
| action: { | ||
| cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5", | ||
| inactiveIcon: "text-muted-foreground", | ||
| }, | ||
| blocked: { | ||
| cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5", | ||
| inactiveIcon: "text-muted-foreground", | ||
| }, | ||
| }; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The STATUS_META object defines identical cardClass values for all three status types ("done", "action", and "blocked"). This makes the status field redundant since it doesn't affect the card styling. Either differentiate the card classes based on status, or remove the unused status field from the type definition if the styling is intentionally the same.
If these styles are meant to be different, consider updating them like:
const STATUS_META: Record<StepStatus, { cardClass: string, inactiveIcon: string }> = {
done: {
cardClass: "border-emerald-500/30 bg-emerald-500/5 transition-all duration-300 hover:shadow-lg dark:border-emerald-500/40",
inactiveIcon: "text-emerald-500 dark:text-emerald-400",
},
action: {
cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5",
inactiveIcon: "text-muted-foreground",
},
blocked: {
cardClass: "border-muted/30 bg-muted/5 transition-all duration-300 dark:border-muted/40",
inactiveIcon: "text-muted-foreground",
},
};| setExpandedStepId((current) => (current === stepId ? null : stepId)); | ||
| }; | ||
|
|
||
| const handleItemClick = (stepId: StepId, itemId: string) => { |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handleItemClick function is always passed to StepCard but only uses the itemId parameter without using it. It immediately calls toggleStepCompletion(stepId) regardless of which specific item was clicked. This could be confusing since the function signature suggests per-item behavior but actually performs per-step behavior.
Consider either:
- Renaming to clarify it's a step-level action:
onStepItemClick - Or removing the
itemIdparameter if it's not needed:
const handleItemClick = (stepId: StepId) => {
toggleStepCompletion(stepId);
};And update the call site to: onItemClick={() => handleItemClick(step.id)}
| const handleItemClick = (stepId: StepId, itemId: string) => { | |
| const handleItemClick = (stepId: StepId) => { |
| <CardDescription | ||
| className={cn( | ||
| "text-sm transition-opacity duration-300 ease-in-out", | ||
| props.isExpanded ? "opacity-100" : "opacity-0" | ||
| )} | ||
| > | ||
| {props.step.subtitle} | ||
| </CardDescription> |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CardDescription has conditional opacity that transitions between opacity-0 when collapsed and opacity-100 when expanded. However, when collapsed (opacity-0), the subtitle text is still rendered in the DOM and takes up space, which could cause layout issues. The text becomes invisible but still occupies vertical space.
Consider using display: none or removing it from the DOM when collapsed:
{props.isExpanded && (
<CardDescription className="text-sm">
{props.step.subtitle}
</CardDescription>
)}Or use a height transition instead of just opacity if you want the animation effect.
| <CardDescription | |
| className={cn( | |
| "text-sm transition-opacity duration-300 ease-in-out", | |
| props.isExpanded ? "opacity-100" : "opacity-0" | |
| )} | |
| > | |
| {props.step.subtitle} | |
| </CardDescription> | |
| {props.isExpanded && ( | |
| <CardDescription className="text-sm"> | |
| {props.step.subtitle} | |
| </CardDescription> | |
| )} |
| <button | ||
| type="button" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| props.onToggle(); | ||
| }} | ||
| className="flex shrink-0 items-center justify-center rounded-md p-1.5 transition-colors hover:bg-accent" | ||
| aria-label={props.isExpanded ? "Collapse section" : "Expand section"} | ||
| > | ||
| {props.isExpanded ? ( | ||
| <ChevronUp className="h-5 w-5 text-muted-foreground" /> | ||
| ) : ( | ||
| <ChevronDown className="h-5 w-5 text-muted-foreground" /> | ||
| )} | ||
| </button> |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The chevron toggle button in the CardHeader has both onClick handlers - one on the button itself (line 561-563) and one on the parent CardHeader (line 526). The button's handler calls e.stopPropagation() to prevent the parent's handler from firing, but this creates redundancy since both handlers call the same props.onToggle() function.
The e.stopPropagation() is unnecessary here. Consider simplifying by removing the button's onClick handler and letting it bubble to the parent:
<button
type="button"
className="flex shrink-0 items-center justify-center rounded-md p-1.5 transition-colors hover:bg-accent"
aria-label={props.isExpanded ? "Collapse section" : "Expand section"}
>| <CardFooter className="flex justify-end"> | ||
| {getActionButton()} | ||
| </CardFooter> |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The action button is displayed in the CardFooter for all steps, but it's only relevant for the "project" and "keys" steps. For other steps (env-vars, deploy, verify), getActionButton() returns null, which still renders an empty CardFooter taking up space.
Consider conditionally rendering the CardFooter only when there's an action button:
{getActionButton() && (
<CardFooter className="flex justify-end">
{getActionButton()}
</CardFooter>
)}Or cache the button result to avoid calling the function twice:
const actionButton = getActionButton();
// ...
{actionButton && (
<CardFooter className="flex justify-end">
{actionButton}
</CardFooter>
)}| {props.error && ( | ||
| <Alert variant="destructive"> | ||
| <AlertTitle>Could not generate keys</AlertTitle> | ||
| <AlertDescription>{props.error}</AlertDescription> | ||
| </Alert> | ||
| )} |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error alert is rendered inside the step's content area but only applies to the "keys" step. However, the error prop is passed to all StepCard instances (line 397), causing the error to potentially display in steps where it's not relevant.
Consider only passing the error prop when rendering the "keys" step:
<StepCard
step={step}
isExpanded={isExpanded}
onToggle={() => handleStepToggle(step.id)}
onGenerateKeys={step.id === "keys" && !keys ? handleGenerateKeys : undefined}
isGenerating={isGenerating}
error={step.id === "keys" ? error : null}
onItemClick={(itemId) => handleItemClick(step.id, itemId)}
/>| function randomInRange(min: number, max: number) { | ||
| return Math.random() * (max - min) + min; | ||
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The randomInRange function is defined inside the effect callback on line 278-280. Since this function doesn't depend on any variables from the effect scope and is only used within the interval callback, it would be more efficient and clearer to define it outside the effect or even outside the component to avoid recreating it on every completion.
Consider moving it outside the effect:
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min;
}
// Trigger confetti when all tasks are completed
useEffect(() => {
// ... rest of the effect
}, [vercelProgress.completed, vercelProgress.total]);| useEffect(() => { | ||
| const allCompleted = vercelProgress.completed === vercelProgress.total && vercelProgress.total > 0; | ||
| const prevAllCompleted = prevAllCompletedRef.current; | ||
|
|
||
| // Only trigger confetti when completion changes from false to true | ||
| if (prevAllCompleted !== undefined && !prevAllCompleted && allCompleted) { | ||
| // Create a confetti effect dropping from the top | ||
| const duration = 3000; | ||
| const animationEnd = Date.now() + duration; | ||
| const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 9999 }; | ||
|
|
||
| function randomInRange(min: number, max: number) { | ||
| return Math.random() * (max - min) + min; | ||
| } | ||
|
|
||
| const interval = setInterval(() => { | ||
| const timeLeft = animationEnd - Date.now(); | ||
|
|
||
| if (timeLeft <= 0) { | ||
| clearInterval(interval); | ||
| return; | ||
| } | ||
|
|
||
| const particleCount = 50 * (timeLeft / duration); | ||
| const result = confetti.default({ | ||
| ...defaults, | ||
| particleCount, | ||
| origin: { x: randomInRange(0.1, 0.9), y: 0 }, | ||
| }); | ||
| if (result) { | ||
| runAsynchronously(result, { noErrorLogging: true }); | ||
| } | ||
| }, 250); | ||
|
|
||
| // Cleanup interval on unmount or when completion changes | ||
| return () => { | ||
| clearInterval(interval); | ||
| }; | ||
| } | ||
|
|
||
| // Update the ref to track the current completion state | ||
| prevAllCompletedRef.current = allCompleted; | ||
| }, [vercelProgress.completed, vercelProgress.total]); |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The confetti effect interval is not properly cleaned up when the component unmounts before all tasks are completed. The cleanup function on line 302-304 only runs when the effect dependencies change or when allCompleted becomes true. If the component unmounts while the confetti is running (e.g., user navigates away), the interval will continue running, potentially causing memory leaks.
Consider storing the interval in a ref and cleaning it up in the effect's cleanup function unconditionally:
useEffect(() => {
const allCompleted = vercelProgress.completed === vercelProgress.total && vercelProgress.total > 0;
const prevAllCompleted = prevAllCompletedRef.current;
if (prevAllCompleted !== undefined && !prevAllCompleted && allCompleted) {
const duration = 3000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 9999 };
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min;
}
const interval = setInterval(() => {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
clearInterval(interval);
return;
}
const particleCount = 50 * (timeLeft / duration);
const result = confetti.default({
...defaults,
particleCount,
origin: { x: randomInRange(0.1, 0.9), y: 0 },
});
if (result) {
runAsynchronously(result, { noErrorLogging: true });
}
}, 250);
return () => clearInterval(interval);
}
prevAllCompletedRef.current = allCompleted;
}, [vercelProgress.completed, vercelProgress.total]);
https://www.loom.com/share/aadb8db65e034f4dbe000b1e4e3cbb37