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:
commit
621ca8926e
@ -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>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 =
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user