Merge pull request #150 from snowball-tools/andrehadianto/T-4912-create-project-create-repository-page

[T-4912] create project create repository page
This commit is contained in:
Andre Hadianto 2024-03-06 09:39:33 +08:00 committed by GitHub
commit 621ca8926e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 175 additions and 117 deletions

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LinkChainIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M9.75027 5.52371L10.7168 4.55722C13.1264 2.14759 17.0332 2.14759 19.4428 4.55722C21.8524 6.96684 21.8524 10.8736 19.4428 13.2832L18.4742 14.2519M5.52886 9.74513L4.55722 10.7168C2.14759 13.1264 2.1476 17.0332 4.55722 19.4428C6.96684 21.8524 10.8736 21.8524 13.2832 19.4428L14.2478 18.4782M9.5 14.5L14.5 9.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</CustomIcon>
);
};

View File

@ -40,6 +40,7 @@ export * from './GithubStrokeIcon';
export * from './BranchStrokeIcon';
export * from './StorageIcon';
export * from './LinkIcon';
export * from './LinkChainIcon';
export * from './CursorBoxIcon';
export * from './CommitIcon';
export * from './RocketIcon';

View File

@ -47,15 +47,15 @@ export const Input = ({
helperIcon: helperIconCls,
} = inputTheme({ ...styleProps });
const renderLabels = useMemo(
() => (
<div className="space-y-1">
const renderLabels = useMemo(() => {
if (!label && !description) return null;
return (
<div className="flex flex-col gap-y-1">
<p className={labelCls()}>{label}</p>
<p className={descriptionCls()}>{description}</p>
</div>
),
[labelCls, descriptionCls, label, description],
);
}, [labelCls, descriptionCls, label, description]);
const renderLeftIcon = useMemo(() => {
return (
@ -73,8 +73,9 @@ export const Input = ({
);
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
const renderHelperText = useMemo(
() => (
const renderHelperText = useMemo(() => {
if (!helperText) return null;
return (
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
@ -82,12 +83,11 @@ export const Input = ({
})}
<p>{helperText}</p>
</div>
),
[cloneIcon, state, helperIconCls, helperText, helperTextCls],
);
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
return (
<div className="flex flex-col gap-2 w-full">
<div className="flex flex-col gap-y-2">
{renderLabels}
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}

View File

@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
export const radioTheme = tv({
slots: {
root: ['flex', 'gap-3', 'flex-wrap'],
root: ['flex', 'gap-3'],
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
radio: [
@ -39,15 +39,34 @@ export const radioTheme = tv({
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
],
icon: ['w-[18px]', 'h-[18px]'],
},
variants: {
orientation: {
vertical: { root: ['flex-col'] },
horizontal: { root: ['flex-row'] },
},
variant: {
unstyled: {},
card: {
wrapper: [
'px-4',
'py-3',
'rounded-lg',
'border',
'border-border-interactive',
'bg-controls-tertiary',
'shadow-button',
'w-full',
'cursor-pointer',
],
label: ['select-none', 'cursor-pointer'],
},
},
},
defaultVariants: {
orientation: 'vertical',
variant: 'unstyled',
},
});

View File

@ -49,14 +49,15 @@ export const Radio = ({
className,
options,
orientation,
variant,
...props
}: RadioProps) => {
const { root } = radioTheme({ orientation });
const { root } = radioTheme({ orientation, variant });
return (
<RadixRoot {...props} className={root({ className })}>
{options.map((option) => (
<RadioItem key={option.value} {...option} />
<RadioItem key={option.value} variant={variant} {...option} />
))}
</RadixRoot>
);

View File

@ -1,13 +1,16 @@
import React, { ComponentPropsWithoutRef } from 'react';
import React, { ReactNode, ComponentPropsWithoutRef } from 'react';
import {
Item as RadixRadio,
Indicator as RadixIndicator,
RadioGroupItemProps,
RadioGroupIndicatorProps,
} from '@radix-ui/react-radio-group';
import { radioTheme } from './Radio.theme';
import { RadioTheme, radioTheme } from './Radio.theme';
import { cloneIcon } from 'utils/cloneIcon';
export interface RadioItemProps extends RadioGroupItemProps {
export interface RadioItemProps
extends RadioGroupItemProps,
Pick<RadioTheme, 'variant'> {
/**
* The wrapper props of the radio item.
* You can use this prop to customize the wrapper props.
@ -27,6 +30,10 @@ export interface RadioItemProps extends RadioGroupItemProps {
* The id of the radio item.
*/
id?: string;
/**
* The left icon of the radio item.
*/
leftIcon?: ReactNode;
/**
* The label of the radio item.
*/
@ -41,18 +48,29 @@ export const RadioItem = ({
wrapperProps,
labelProps,
indicatorProps,
leftIcon,
label,
id,
variant,
...props
}: RadioItemProps) => {
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
const {
wrapper,
label: labelClass,
radio,
indicator,
icon,
} = radioTheme({ variant });
// Generate a unique id for the radio item from the label if the id is not provided
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
const componentId = id ?? kebabCaseLabel;
return (
<div className={wrapper({ className: wrapperProps?.className })}>
<label
htmlFor={componentId}
className={wrapper({ className: wrapperProps?.className })}
>
<RadixRadio {...props} className={radio({ className })} id={componentId}>
<RadixIndicator
forceMount
@ -60,15 +78,20 @@ export const RadioItem = ({
className={indicator({ className: indicatorProps?.className })}
/>
</RadixRadio>
{leftIcon && (
<span>
{cloneIcon(leftIcon, { className: icon(), 'aria-hidden': true })}
</span>
)}
{label && (
<label
{...labelProps}
className={labelClass({ className: labelProps?.className })}
htmlFor={componentId}
className={labelClass({ className: labelProps?.className })}
>
{label}
</label>
)}
</div>
</label>
);
};

View File

@ -266,15 +266,15 @@ export const Select = ({
onClear?.();
};
const renderLabels = useMemo(
() => (
<div className="space-y-1">
const renderLabels = useMemo(() => {
if (!label && !description) return null;
return (
<div className="flex flex-col gap-y-1">
<p className={theme.label()}>{label}</p>
<p className={theme.description()}>{description}</p>
</div>
),
[theme, label, description],
);
}, [theme, label, description]);
const renderLeftIcon = useMemo(() => {
return (
@ -302,8 +302,9 @@ export const Select = ({
);
}, [cloneIcon, theme, rightIcon]);
const renderHelperText = useMemo(
() => (
const renderHelperText = useMemo(() => {
if (!helperText) return null;
return (
<div className={theme.helperText()}>
{error &&
cloneIcon(<WarningIcon className={theme.helperIcon()} />, {
@ -311,9 +312,8 @@ export const Select = ({
})}
<p>{helperText}</p>
</div>
),
[cloneIcon, error, theme, helperText],
);
}, [cloneIcon, error, theme, helperText]);
const isMultipleHasValue = multiple && selectedItems.length > 0;
const isMultipleHasValueButNotSearchable =

View File

@ -6,9 +6,13 @@ import {
useSearchParams,
} from 'react-router-dom';
import { Avatar } from '@material-tailwind/react';
import templates from '../../../../assets/templates';
import {
LinkChainIcon,
TemplateIcon,
TemplateIconType,
} from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading';
import { Steps } from 'components/shared/Steps';
// TODO: Set dynamic route for template and load details from DB
@ -44,19 +48,24 @@ const CreateWithTemplate = () => {
return (
<div className="flex flex-col items-center">
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6 items-center">
<Avatar variant="rounded" src="/gray.png" placeholder={''} />
<div className="grow px-2">{template?.name}</div>
<div className="flex flex-col lg:flex-row justify-between w-5/6 my-4 bg-base-bg-alternate rounded-xl p-6 gap-3 items-start lg:items-center">
<div className="flex items-center gap-3">
<TemplateIcon type={template?.icon as TemplateIconType} size={48} />
<Heading className="font-medium">{template?.name}</Heading>
</div>
<div>
<a
href={`https://github.com/${template?.repoFullName}`}
target="_blank"
rel="noreferrer"
className="flex gap-1.5 items-center text-sm"
>
^{' '}
<LinkChainIcon size={18} />
<span className="underline">
{Boolean(template?.repoFullName)
? template?.repoFullName
: 'Template not supported'}
</span>
</a>
</div>
</div>

View File

@ -1,15 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
import toast from 'react-hot-toast';
import assert from 'assert';
import { Button, Option, Typography } from '@material-tailwind/react';
import { useOctokit } from '../../../../../context/OctokitContext';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import AsyncSelect from '../../../../../components/shared/AsyncSelect';
import { Template } from '../../../../../types';
import { Heading } from 'components/shared/Heading';
import { Input } from 'components/shared/Input';
import { Select, SelectOption } from 'components/shared/Select';
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon';
import { Checkbox } from 'components/shared/Checkbox';
import { Button } from 'components/shared/Button';
type SubmitRepoValues = {
framework: string;
@ -93,7 +96,7 @@ const CreateRepo = () => {
fetchUserAndOrgs();
}, [octokit]);
const { register, handleSubmit, control, reset } = useForm<SubmitRepoValues>({
const { handleSubmit, control, reset } = useForm<SubmitRepoValues>({
defaultValues: {
framework: 'React',
repoName: '',
@ -110,87 +113,68 @@ const CreateRepo = () => {
return (
<form onSubmit={handleSubmit(submitRepoHandler)}>
<div className="mb-2">
<Typography variant="h6" placeholder={''}>
Create a repository
</Typography>
<Typography color="gray" placeholder={''}>
The project will be cloned into this repository
</Typography>
</div>
<div className="mb-2">
<h5>Framework</h5>
<div className="flex items-center gap-2">
<label className="inline-flex items-center w-1/2 border rounded-lg p-2">
<input
type="radio"
{...register('framework')}
value="React"
className="h-5 w-5 text-indigo-600 rounded"
/>
<span className="ml-2">^React</span>
</label>
<label className="inline-flex items-center w-1/2 border rounded-lg p-2">
<input
type="radio"
{...register('framework')}
className="h-5 w-5 text-indigo-600 rounded"
value="Next"
/>
<span className="ml-2">^Next</span>
</label>
</div>
</div>
<div className="mb-2">
<h5>Git account</h5>
<div className="flex flex-col gap-4 lg:gap-7 w-full">
<div>
<Heading as="h3" className="text-lg font-medium">
Create a repository
</Heading>
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The project will be cloned into this repository
</Heading>
</div>
<div className="flex flex-col justify-start gap-3">
<span className="text-sm text-elements-high-em">Git account</span>
<Controller
name="account"
control={control}
render={({ field }) => (
<AsyncSelect {...field}>
{gitAccounts.map((account, key) => (
<Option key={key} value={account}>
^ {account}
</Option>
))}
</AsyncSelect>
render={({ field: { value, onChange } }) => (
<Select
value={{ value } as SelectOption}
onChange={(value) => onChange((value as SelectOption).value)}
options={
gitAccounts.map((account) => ({
value: account,
label: account,
})) ?? []
}
/>
)}
/>
</div>
<div className="flex flex-col justify-start gap-3">
<span className="text-sm text-elements-high-em">Name the repo</span>
<Controller
name="repoName"
control={control}
render={({ field: { value, onChange } }) => (
<Input value={value} onChange={onChange} />
)}
/>
</div>
<div className="mb-2">
<h5>Name the repo</h5>
<div>
<input
type="text"
className="border border-gray-300 rounded p-2 w-full focus:border-blue-300 focus:outline-none focus:shadow-outline-blue"
placeholder=""
{...register('repoName')}
<Controller
name="isPrivate"
control={control}
render={({ field: { value, onChange } }) => (
<Checkbox
label="Make this repo private"
checked={value}
onCheckedChange={onChange}
/>
)}
/>
</div>
</div>
<div className="mb-2">
<label className="inline-flex items-center">
<input
type="checkbox"
className="h-5 w-5 text-indigo-600 rounded"
{...register('isPrivate')}
/>
<span className="ml-2">Make this repo private</span>
</label>
</div>
<div className="mb-2">
<div>
<Button
className="bg-blue-500 rounded-xl p-2"
type="submit"
size="lg"
disabled={!Boolean(template.repoFullName) || isLoading}
loading={isLoading}
placeholder={''}
rightIcon={<ArrowRightCircleFilledIcon />}
>
Deploy ^
Deploy
</Button>
</div>
</div>
</form>
);
};