Skip to content

Commit

Permalink
onlook-dev#533 [feat] Swipe-animate the project setup flow using Fram…
Browse files Browse the repository at this point in the history
…er Motion
  • Loading branch information
0xsommer committed Oct 16, 2024
1 parent 29270d5 commit 99786e5
Show file tree
Hide file tree
Showing 11 changed files with 735 additions and 557 deletions.
67 changes: 67 additions & 0 deletions app/src/components/ui/motion-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import { motion, HTMLMotionProps } from 'framer-motion';

import { cn } from '@/lib/utils';

type MotionDivProps = HTMLMotionProps<'div'>;

const MotionCard = React.forwardRef<HTMLDivElement, MotionDivProps>(
({ className, style, ...props }, ref) => (
<motion.div
ref={ref}
className={cn('relative', className)}
style={{
borderRadius: '12px',
backdropFilter: 'blur(12px)',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
boxShadow: `
-0.5px 0px 0px 0 rgba(255, 255, 255, 0.3),
0px -0.5px 0px 0 rgba(255, 255, 255, 0.3)
`,
color: 'var(--card-foreground)',
...style,
}}
{...props}
>
{props.children}
</motion.div>
),
);
MotionCard.displayName = 'MotionCard';

const MotionCardHeader = React.forwardRef<HTMLDivElement, MotionDivProps>(
({ className, ...props }, ref) => (
<motion.div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
);
MotionCardHeader.displayName = 'MotionCardHeader';

const MotionCardTitle = React.forwardRef<HTMLHeadingElement, HTMLMotionProps<'h3'>>(
({ className, ...props }, ref) => (
<motion.h3 ref={ref} className={cn('text-title3', className)} {...props} />
),
);
MotionCardTitle.displayName = 'MotionCardTitle';

const MotionCardDescription = React.forwardRef<HTMLParagraphElement, HTMLMotionProps<'p'>>(
({ className, ...props }, ref) => (
<motion.p ref={ref} className={cn('text-regular text-muted-foreground', className)} {...props} />
),
);
MotionCardDescription.displayName = 'MotionCardDescription';

const MotionCardContent = React.forwardRef<HTMLDivElement, MotionDivProps>(
({ className, ...props }, ref) => (
<motion.div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
),
);
MotionCardContent.displayName = 'MotionCardContent';

const MotionCardFooter = React.forwardRef<HTMLDivElement, MotionDivProps>(
({ className, ...props }, ref) => (
<motion.div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
);
MotionCardFooter.displayName = 'MotionCardFooter';

export { MotionCard, MotionCardHeader, MotionCardFooter, MotionCardTitle, MotionCardDescription, MotionCardContent };
150 changes: 77 additions & 73 deletions app/src/routes/projects/ProjectsTab/Create/Load/SelectFolder.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { CardTitle, CardDescription } from '@/components/ui/card';
import { getNameFromPath } from '@/routes/projects/helpers';
import { MinusCircledIcon } from '@radix-ui/react-icons';
import { StepProps } from '..';
import { StepComponent } from '../withStepProps';
import { MainChannels } from '/common/constants';

export const LoadSelectFolder = ({
props: { projectData, setProjectData, currentStep, totalSteps, prevStep, nextStep },
}: {
props: StepProps;
}) => {
async function pickProjectFolder() {
const path = (await window.api.invoke(MainChannels.PICK_COMPONENTS_DIRECTORY)) as
| string
| null;
const LoadSelectFolder: StepComponent = ({ props, variant }) => {
const { projectData, setProjectData, prevStep, nextStep } = props;

if (path == null) {
return;
}
async function pickProjectFolder() {
const path = (await window.api.invoke(MainChannels.PICK_COMPONENTS_DIRECTORY)) as string | null;
if (path == null) return;
setProjectData({
...projectData,
folderPath: path,
Expand All @@ -41,64 +29,80 @@ export const LoadSelectFolder = ({
window.api.invoke(MainChannels.OPEN_IN_EXPLORER, projectData.folderPath);
}

return (
<Card className="w-[30rem] backdrop-blur-md bg-background/30">
<CardHeader>
<CardTitle>{'Select your project folder'}</CardTitle>
<CardDescription>{'This is where we’ll reference your App'}</CardDescription>
</CardHeader>
<CardContent className="min-h-24 flex items-center w-full ">
{projectData.folderPath ? (
<div className="w-full flex flex-row items-center border-[0.5px] bg-background-onlook/60 px-4 py-5 rounded">
<div className="flex flex-col text-sm gap-1 break-all">
<p className="text-regularPlus">{projectData.name}</p>
<button
className="hover:underline text-mini text-foreground-onlook text-start"
onClick={handleClickPath}
>
{projectData.folderPath}
</button>
</div>
<Button
className="ml-auto w-10 h-10"
variant={'ghost'}
size={'icon'}
onClick={() => {
setProjectData({
...projectData,
folderPath: undefined,
});
}}
const renderHeader = () => (
<>
<CardTitle>{'Select your project folder'}</CardTitle>
<CardDescription>{'This is where we\'ll reference your App'}</CardDescription>
</>
);

const renderContent = () => (
<>
{projectData.folderPath ? (
<div className="w-full flex flex-row items-center border-[0.5px] bg-background-onlook/60 px-4 py-5 rounded">
<div className="flex flex-col text-sm gap-1 break-all">
<p className="text-regularPlus">{projectData.name}</p>
<button
className="hover:underline text-mini text-foreground-onlook text-start"
onClick={handleClickPath}
>
<MinusCircledIcon />
</Button>
{projectData.folderPath}
</button>
</div>
) : (
<Button
className="w-full h-20 text-regularPlus text-foreground-onlook border-[0.5px] bg-background-onlook/50 hover:bg-background-onlook/60"
variant={'outline'}
onClick={pickProjectFolder}
>
{'Click to select your folder'}
</Button>
)}
</CardContent>
<CardFooter className="text-sm">
<p className="text-foreground-onlook">{`${currentStep + 1} of ${totalSteps}`}</p>
<div className="flex ml-auto gap-2">
<Button type="button" onClick={prevStep} variant="ghost">
Back
</Button>
<Button
disabled={!projectData.folderPath}
type="button"
onClick={verifyFolder}
variant="outline"
className="ml-auto w-10 h-10"
variant={'ghost'}
size={'icon'}
onClick={() => {
setProjectData({
...projectData,
folderPath: undefined,
});
}}
>
Next
<MinusCircledIcon />
</Button>
</div>
</CardFooter>
</Card>
) : (
<Button
className="w-full h-20 text-regularPlus text-foreground-onlook border-[0.5px] bg-background-onlook/50 hover:bg-background-onlook/60"
variant={'outline'}
onClick={pickProjectFolder}
>
{'Click to select your folder'}
</Button>
)}
</>
);

const renderFooter = () => (
<>
<Button type="button" onClick={prevStep} variant="ghost">
Back
</Button>
<Button
disabled={!projectData.folderPath}
type="button"
onClick={verifyFolder}
variant="outline"
>
Next
</Button>
</>
);

switch (variant) {
case 'header':
return renderHeader();
case 'content':
return renderContent();
case 'footer':
return renderFooter();
}
};

LoadSelectFolder.Header = (props) => <LoadSelectFolder props={props} variant="header" />;
LoadSelectFolder.Content = (props) => <LoadSelectFolder props={props} variant="content" />;
LoadSelectFolder.Footer = (props) => <LoadSelectFolder props={props} variant="footer" />;

export { LoadSelectFolder };
92 changes: 52 additions & 40 deletions app/src/routes/projects/ProjectsTab/Create/Load/SetUrl.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Card,
Expand All @@ -9,15 +10,12 @@ import {
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useState } from 'react';
import { StepProps } from '..';
import { StepComponent } from '../withStepProps';
import { MainChannels } from '/common/constants';

export const LoadSetUrl = ({
props: { projectData, setProjectData, currentStep, totalSteps, prevStep, nextStep },
}: {
props: StepProps;
}) => {
const LoadSetUrl: StepComponent = ({ props, variant }) => {
const { projectData, setProjectData, prevStep, nextStep } = props;
const [inputValue, setInputValue] = useState<string>(projectData.url || '');
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -49,41 +47,55 @@ export const LoadSetUrl = ({
prevStep();
}

return (
<Card className="w-[30rem] backdrop-blur-md bg-background/30">
<CardHeader>
const renderHeader = () => (
<>
<CardTitle>{'Set your project URL'}</CardTitle>
<CardDescription>{'Where is your project running locally?'}</CardDescription>
</CardHeader>
<CardContent className="h-24 flex items-center w-full">
<div className="flex flex-col w-full gap-2">
<Label htmlFor="text">Local Url</Label>
<Input
className="bg-secondary"
value={inputValue}
type="text"
placeholder="http://localhost:3000"
onInput={handleUrlInput}
/>
<p className="text-red-500 text-sm">{error || ''}</p>
</div>
</CardContent>
<CardFooter className="text-sm">
<p className="text-foreground-onlook">{`${currentStep + 1} of ${totalSteps}`}</p>
<div className="flex ml-auto gap-2">
<Button type="button" onClick={goBack} variant="ghost">
Back
</Button>
<Button
disabled={!projectData.url || projectData.url.length === 0}
type="button"
onClick={nextStep}
variant="outline"
>
Complete setup
</Button>
</div>
</CardFooter>
</Card>
</>
);

const renderContent = () => (
<div className="flex flex-col w-full gap-2">
<Label htmlFor="text">Local Url</Label>
<Input
className="bg-secondary"
value={inputValue}
type="text"
placeholder="http://localhost:3000"
onInput={handleUrlInput}
/>
<p className="text-red-500 text-sm">{error || ''}</p>
</div>
);

const renderFooter = () => (
<>
<Button type="button" onClick={goBack} variant="ghost">
Back
</Button>
<Button
disabled={!projectData.url || projectData.url.length === 0}
type="button"
onClick={nextStep}
variant="outline"
>
Complete setup
</Button>
</>
);

switch (variant) {
case 'header':
return renderHeader();
case 'content':
return renderContent();
case 'footer':
return renderFooter();
}
};

LoadSetUrl.Header = (props) => <LoadSetUrl props={props} variant="header" />;
LoadSetUrl.Content = (props) => <LoadSetUrl props={props} variant="content" />;
LoadSetUrl.Footer = (props) => <LoadSetUrl props={props} variant="footer" />;

export { LoadSetUrl };
Loading

0 comments on commit 99786e5

Please sign in to comment.