mirror of
https://github.com/snowball-tools/snowballtools-base.git
synced 2024-11-18 04:56:18 +00:00
Merge branch 'main' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4917-project-deployments-layout-and-empty-state
This commit is contained in:
commit
462d247a86
@ -49,7 +49,7 @@
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
|
@ -101,7 +101,12 @@ const DeployStep = ({
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CheckRoundFilledIcon className="text-elements-success" size={18} />
|
||||
<div className="w-4.5 h-4.5 grid place-content-center">
|
||||
<CheckRoundFilledIcon
|
||||
className="text-elements-success"
|
||||
size={15}
|
||||
/>
|
||||
</div>
|
||||
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,81 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitRepositoryDetails } from '../../../types';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug!, {
|
||||
name: `${repository.owner!.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch!,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
|
||||
if (Boolean(addProject)) {
|
||||
setIsLoading(false);
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error('Failed to create project');
|
||||
}
|
||||
}, [client, repository]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
||||
onClick={createProject}
|
||||
>
|
||||
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
|
||||
<GithubIcon />
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div>
|
||||
<span className="text-black">{repository.full_name}</span>
|
||||
{repository.visibility === 'private' && (
|
||||
<Chip
|
||||
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
|
||||
size="sm"
|
||||
value="Private"
|
||||
icon={<LockIcon />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm" placeholder={''}>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRepoCard;
|
@ -0,0 +1,114 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { GitRepositoryDetails } from 'types';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
GithubIcon,
|
||||
LockIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
repository,
|
||||
}) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository || !orgSlug) {
|
||||
return toast({
|
||||
id: 'missing-repository-or-org-slug',
|
||||
title: 'Repository or organization slug is missing',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug, {
|
||||
name: `${repository.owner?.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch as string,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
if (addProject) {
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
|
||||
onClick={createProject}
|
||||
>
|
||||
{/* Icon container */}
|
||||
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
|
||||
<GithubIcon />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 gap-3 flex-wrap">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||
{repository.full_name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs">
|
||||
{repository.updated_at && relativeTimeISO(repository.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
{repository.visibility === 'private' && (
|
||||
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||
<LockIcon />
|
||||
Private
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Right action */}
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4 absolute right-3" />
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
iconOnly
|
||||
className="sm:group-hover:flex hidden absolute right-3"
|
||||
>
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './ProjectRepoCard';
|
@ -3,13 +3,17 @@ import { Octokit } from 'octokit';
|
||||
import assert from 'assert';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { Button, Typography, Option } from '@material-tailwind/react';
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../SearchBar';
|
||||
import ProjectRepoCard from './ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
||||
import AsyncSelect from '../../shared/AsyncSelect';
|
||||
import { GithubIcon } from 'components/shared/CustomIcon';
|
||||
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from 'types';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
GithubIcon,
|
||||
SearchIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
@ -18,9 +22,9 @@ interface RepositoryListProps {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
@ -35,7 +39,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount(user.data.login);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
};
|
||||
|
||||
fetchUserAndOrgs();
|
||||
@ -54,7 +58,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount === gitUser.login) {
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
@ -69,7 +73,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccount === gitUser.login) {
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
per_page: REPOS_PER_PAGE,
|
||||
affiliation: 'owner',
|
||||
@ -78,7 +82,9 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOrg = orgs.find((org) => org.login === selectedAccount);
|
||||
const selectedOrg = orgs.find(
|
||||
(org) => org.login === selectedAccount.value,
|
||||
);
|
||||
assert(selectedOrg, 'Selected org not found in list');
|
||||
|
||||
const result = await octokit.rest.repos.listForOrg({
|
||||
@ -96,7 +102,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const handleResetFilters = useCallback(() => {
|
||||
assert(gitUser, 'Git user is not available');
|
||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||
setSelectedAccount(gitUser.login);
|
||||
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||
}, [gitUser]);
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
@ -107,35 +113,52 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return [gitUser, ...orgs];
|
||||
}, [octokit, orgs, gitUser]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return accounts.map((account) => ({
|
||||
label: account.login,
|
||||
value: account.login,
|
||||
leftIcon: <GithubIcon />,
|
||||
}));
|
||||
}, [accounts]);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex gap-2 mb-2 items-center">
|
||||
<div className="basis-1/3">
|
||||
<AsyncSelect
|
||||
<section className="space-y-3">
|
||||
{/* Dropdown and search */}
|
||||
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||
<div className="lg:basis-1/3 w-full">
|
||||
<Select
|
||||
options={options}
|
||||
placeholder="Select a repository"
|
||||
value={selectedAccount}
|
||||
onChange={(value) => setSelectedAccount(value!)}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<Option key={account.id} value={account.login}>
|
||||
<div className="flex items-center gap-2 justify-start">
|
||||
<GithubIcon /> {account.login}
|
||||
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||
/>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
</div>
|
||||
<div className="basis-2/3 flex-grow flex items-center">
|
||||
<SearchBar
|
||||
<div className="basis-2/3 flex w-full flex-grow">
|
||||
<Input
|
||||
className="w-full"
|
||||
value={searchedRepo}
|
||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
||||
placeholder="Search for repository"
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={(e) => setSearchedRepo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repository list */}
|
||||
{Boolean(repositoryDetails.length) ? (
|
||||
repositoryDetails.map((repo, key) => {
|
||||
return <ProjectRepoCard repository={repo} key={key} />;
|
||||
})
|
||||
<div className="flex flex-col gap-2">
|
||||
{repositoryDetails.map((repo, index) => (
|
||||
<>
|
||||
<ProjectRepoCard repository={repo} key={index} />
|
||||
{/* Horizontal line */}
|
||||
{index !== repositoryDetails.length - 1 && (
|
||||
<div className="border-b border-border-separator/[0.06] w-full" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -151,8 +174,6 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryList;
|
@ -0,0 +1 @@
|
||||
export * from './RepositoryList';
|
@ -55,9 +55,9 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
|
||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative cursor-pointer',
|
||||
{
|
||||
'cursor-default': template?.isComingSoon,
|
||||
},
|
||||
@ -86,6 +86,6 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -11,7 +11,7 @@ export const Activity = ({
|
||||
activities: GitCommitWithBranch[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-2 mr-1">
|
||||
<div className="col-span-5 md:col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
|
@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden', 'shrink-0'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
|
@ -3,17 +3,11 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<CustomIcon width="20" height="20" viewBox="0 0 20 20" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
||||
d="M10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0ZM13.774 8.13327C14.1237 7.70582 14.0607 7.0758 13.6332 6.72607C13.2058 6.37635 12.5758 6.43935 12.226 6.86679L8.42576 11.5116L7.20711 10.2929C6.81658 9.9024 6.18342 9.9024 5.79289 10.2929C5.40237 10.6834 5.40237 11.3166 5.79289 11.7071L7.79289 13.7071C7.99267 13.9069 8.26764 14.0129 8.54981 13.9988C8.83199 13.9847 9.09505 13.8519 9.27396 13.6333L13.774 8.13327Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
|
@ -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 './CrossCircleIcon';
|
||||
export * from './RefreshIcon';
|
||||
|
@ -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 w-full">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
@ -85,7 +85,7 @@ export const selectTheme = tv({
|
||||
size: {
|
||||
md: {
|
||||
container: ['min-h-11'],
|
||||
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4', 'py-1'],
|
||||
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4'],
|
||||
icon: ['h-[18px]', 'w-[18px]'],
|
||||
helperText: 'text-sm',
|
||||
helperIcon: ['h-5', 'w-5'],
|
||||
@ -93,7 +93,7 @@ export const selectTheme = tv({
|
||||
},
|
||||
sm: {
|
||||
container: ['min-h-8'],
|
||||
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3', 'py-0.5'],
|
||||
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
helperText: 'text-xs',
|
||||
helperIcon: ['h-4', 'w-4'],
|
||||
|
@ -134,7 +134,9 @@ export const Select = ({
|
||||
const theme = selectTheme({ size, error, variant, orientation });
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(
|
||||
(value as SelectOption) || null,
|
||||
);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
||||
'bottom',
|
||||
@ -165,22 +167,6 @@ export const Select = ({
|
||||
}
|
||||
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
||||
|
||||
useEffect(() => {
|
||||
// If multiple selection is enabled, ensure the internal state is an array
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
// Directly use the provided array
|
||||
setSelectedItems(value);
|
||||
} else {
|
||||
// Reset or set to empty array if the value is not an array
|
||||
setSelectedItems([]);
|
||||
}
|
||||
} else {
|
||||
// For single selection, directly set the selected item
|
||||
setSelectedItem(value as SelectOption);
|
||||
}
|
||||
}, [value, multiple]);
|
||||
|
||||
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
|
||||
setSelectedItem(selectedItem);
|
||||
setInputValue(selectedItem ? selectedItem.label : '');
|
||||
@ -193,9 +179,9 @@ export const Select = ({
|
||||
addSelectedItem,
|
||||
removeSelectedItem,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
reset,
|
||||
} = useMultipleSelection<SelectOption>({
|
||||
selectedItems: multiple ? (value as SelectOption[]) : [],
|
||||
onSelectedItemsChange: multiple
|
||||
? undefined
|
||||
: ({ selectedItems }) => {
|
||||
@ -233,6 +219,7 @@ export const Select = ({
|
||||
openMenu,
|
||||
} = useCombobox({
|
||||
items: filteredItems,
|
||||
selectedItem: multiple ? null : (value as SelectOption) || null,
|
||||
// @ts-expect-error – there are two params but we don't need the second one
|
||||
isItemDisabled: (item) => item.disabled,
|
||||
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
||||
@ -264,7 +251,6 @@ export const Select = ({
|
||||
setInputValue('');
|
||||
}
|
||||
},
|
||||
selectedItem: multiple ? null : selectedItem,
|
||||
// TODO: Make the input value empty when the dropdown is open, has a value, it is not multiple, and searchable
|
||||
itemToString: (item) => (item && !multiple ? item.label : ''),
|
||||
});
|
||||
@ -280,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 (
|
||||
@ -319,8 +305,9 @@ export const Select = ({
|
||||
);
|
||||
}, [cloneIcon, theme, rightIcon, selectedItem, selectedItems, clearable]);
|
||||
|
||||
const renderHelperText = useMemo(
|
||||
() => (
|
||||
const renderHelperText = useMemo(() => {
|
||||
if (!helperText) return null;
|
||||
return (
|
||||
<div className={theme.helperText()}>
|
||||
{error &&
|
||||
cloneIcon(<WarningIcon className={theme.helperIcon()} />, {
|
||||
@ -328,13 +315,13 @@ export const Select = ({
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
),
|
||||
[cloneIcon, error, theme, helperText],
|
||||
);
|
||||
}, [cloneIcon, error, theme, helperText]);
|
||||
|
||||
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
||||
const isMultipleHasValueButNotSearchable =
|
||||
multiple && !searchable && selectedItems.length > 0;
|
||||
|
||||
const displayPlaceholder = useMemo(() => {
|
||||
if (hideValues && isMultipleHasValue) {
|
||||
return `${selectedItems.length} selected`;
|
||||
@ -391,7 +378,7 @@ export const Select = ({
|
||||
// Add margin to the X icon
|
||||
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
||||
// Add padding if there's a left icon
|
||||
'pl-6': leftIcon,
|
||||
'pl-7': leftIcon,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
|
@ -0,0 +1,52 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const stepTheme = tv({
|
||||
slots: {
|
||||
wrapper: ['relative', 'px-1.5', 'py-1.5', 'flex', 'gap-2', 'items-center'],
|
||||
step: [
|
||||
'bg-base-bg-emphasized',
|
||||
'rounded-full',
|
||||
'w-7',
|
||||
'h-7',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'text-elements-mid-em',
|
||||
'shadow-button',
|
||||
'shrink-0',
|
||||
],
|
||||
label: [
|
||||
'text-sm',
|
||||
'font-sans',
|
||||
'text-elements-mid-em',
|
||||
'whitespace-nowrap',
|
||||
],
|
||||
connector: [],
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: {
|
||||
connector: ['bg-border-interactive-hovered', 'w-px', 'h-3', 'ml-5'],
|
||||
},
|
||||
horizontal: {
|
||||
connector: ['text-border-interactive-hovered', 'h-3', 'w-3'],
|
||||
},
|
||||
},
|
||||
active: {
|
||||
true: {
|
||||
step: ['bg-controls-secondary-hovered', 'text-elements-on-secondary'],
|
||||
label: ['text-elements-high-em'],
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
true: {
|
||||
step: ['text-controls-primary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
});
|
||||
|
||||
export type StepTheme = VariantProps<typeof stepTheme>;
|
67
packages/frontend/src/components/shared/Steps/Step/Step.tsx
Normal file
67
packages/frontend/src/components/shared/Steps/Step/Step.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useCallback, ComponentPropsWithoutRef } from 'react';
|
||||
import { stepTheme, StepTheme } from './Step.theme';
|
||||
import {
|
||||
CheckRoundFilledIcon,
|
||||
ChevronRight,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
export interface StepProps extends ComponentPropsWithoutRef<'li'>, StepTheme {
|
||||
/**
|
||||
* The label for the step
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The index of the step
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* The total number of steps
|
||||
*/
|
||||
currentIndex: number;
|
||||
}
|
||||
|
||||
export const Step = ({
|
||||
label,
|
||||
index,
|
||||
currentIndex,
|
||||
orientation,
|
||||
...props
|
||||
}: StepProps) => {
|
||||
const theme = stepTheme();
|
||||
|
||||
const active = currentIndex === index;
|
||||
const completed = currentIndex > index;
|
||||
|
||||
const renderConnector = useCallback(
|
||||
(index: number) => {
|
||||
if (index === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div aria-hidden className={theme.connector({ orientation })}>
|
||||
{orientation === 'horizontal' && <ChevronRight size={12} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[orientation, theme],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderConnector(index)}
|
||||
<li className={theme.wrapper()} {...props}>
|
||||
{
|
||||
<div className={theme.step({ active, completed })}>
|
||||
{completed ? (
|
||||
<CheckRoundFilledIcon className="w-full h-full" />
|
||||
) : (
|
||||
index
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
<p className={theme.label()}>{label}</p>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './Step';
|
||||
export * from './Step.theme';
|
18
packages/frontend/src/components/shared/Steps/Steps.theme.ts
Normal file
18
packages/frontend/src/components/shared/Steps/Steps.theme.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const stepsTheme = tv({
|
||||
slots: {
|
||||
root: [],
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: { root: ['flex', 'flex-col'] },
|
||||
horizontal: { root: ['flex', 'items-center', 'gap-1'] },
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
});
|
||||
|
||||
export type StepsTheme = VariantProps<typeof stepsTheme>;
|
42
packages/frontend/src/components/shared/Steps/Steps.tsx
Normal file
42
packages/frontend/src/components/shared/Steps/Steps.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { Fragment, ComponentPropsWithoutRef } from 'react';
|
||||
import { stepsTheme, StepsTheme } from './Steps.theme';
|
||||
import { Step, StepProps, StepTheme } from './Step';
|
||||
|
||||
interface StepsProps
|
||||
extends ComponentPropsWithoutRef<'ul'>,
|
||||
StepsTheme,
|
||||
Pick<StepTheme, 'orientation'> {
|
||||
/**
|
||||
* The index of the current step
|
||||
*/
|
||||
currentIndex: number;
|
||||
/**
|
||||
* The steps to render
|
||||
*/
|
||||
steps: Pick<StepProps, 'label'>[];
|
||||
}
|
||||
|
||||
export const Steps = ({
|
||||
currentIndex,
|
||||
steps = [],
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: StepsProps) => {
|
||||
const theme = stepsTheme();
|
||||
|
||||
return (
|
||||
<ul className={theme.root({ class: className, orientation })} {...props}>
|
||||
{steps.map((step, i) => (
|
||||
<Fragment key={i}>
|
||||
<Step
|
||||
{...step}
|
||||
orientation={orientation}
|
||||
currentIndex={currentIndex}
|
||||
index={i + 1}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
2
packages/frontend/src/components/shared/Steps/index.ts
Normal file
2
packages/frontend/src/components/shared/Steps/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Steps';
|
||||
export * from './Steps.theme';
|
@ -71,6 +71,7 @@ export const tabsTheme = tv({
|
||||
'gap-5',
|
||||
'border-b',
|
||||
'border-transparent',
|
||||
'overflow-scroll',
|
||||
// Horizontal – default
|
||||
'data-[orientation=horizontal]:border-border-interactive/10',
|
||||
// Vertical
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
import { renderInputs } from './renders/input';
|
||||
import { RADIO_OPTIONS } from './renders/radio';
|
||||
import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls';
|
||||
import { renderHorizontalSteps, renderVerticalSteps } from './renders/steps';
|
||||
import {
|
||||
renderTabWithBadges,
|
||||
renderTabs,
|
||||
@ -56,6 +57,19 @@ const Page: React.FC = () => {
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Steps */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Steps</h1>
|
||||
<div className="flex flex-col gap-10 items-center justify-center">
|
||||
{renderVerticalSteps()}
|
||||
{renderHorizontalSteps()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Tag */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
|
41
packages/frontend/src/pages/components/renders/steps.tsx
Normal file
41
packages/frontend/src/pages/components/renders/steps.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Steps } from 'components/shared/Steps';
|
||||
|
||||
export const renderVerticalSteps = () => {
|
||||
return (
|
||||
<Steps
|
||||
currentIndex={1}
|
||||
steps={[
|
||||
{
|
||||
label: 'Create repository',
|
||||
},
|
||||
{
|
||||
label: 'Deploy',
|
||||
},
|
||||
{
|
||||
label: `What's next?`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderHorizontalSteps = () => {
|
||||
return (
|
||||
<Steps
|
||||
orientation="horizontal"
|
||||
currentIndex={1}
|
||||
steps={[
|
||||
{
|
||||
label: 'Create repository',
|
||||
},
|
||||
{
|
||||
label: 'Deploy',
|
||||
},
|
||||
{
|
||||
label: `What's next?`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
@ -7,6 +7,7 @@ import {
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { Project as ProjectType } from 'gql-client';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { useOctokit } from '../../../context/OctokitContext';
|
||||
@ -23,6 +24,9 @@ const Id = () => {
|
||||
const client = useGQLClient();
|
||||
const location = useLocation();
|
||||
|
||||
const isDesktopView = useMediaQuery('(min-width: 768px)'); // md:
|
||||
const buttonSize = isDesktopView ? {} : { size: 'sm' as const };
|
||||
|
||||
const [project, setProject] = useState<ProjectType | null>(null);
|
||||
const [repoUrl, setRepoUrl] = useState('');
|
||||
|
||||
@ -65,25 +69,32 @@ const Id = () => {
|
||||
{project ? (
|
||||
<>
|
||||
<div className="px-6 py-4 flex justify-between items-center gap-4">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex items-center justify-center gap-4 overflow-hidden">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="rounded-full h-11 w-11 p-0"
|
||||
iconOnly
|
||||
className="rounded-full h-11 w-11 p-0 shrink-0"
|
||||
aria-label="Go back"
|
||||
leftIcon={<ChevronLeft />}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
<Heading className="text-2xl font-medium">
|
||||
<Heading className="text-2xl font-medium truncate">
|
||||
{project?.name}
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Link to={repoUrl} target="_blank">
|
||||
<Button className="h-11 transition-colors" variant="tertiary">
|
||||
<Button
|
||||
{...buttonSize}
|
||||
className="h-11 transition-colors"
|
||||
variant="tertiary"
|
||||
>
|
||||
Open repo
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="h-11 transition-colors">Go to app</Button>
|
||||
<Button {...buttonSize} className="h-11 transition-colors">
|
||||
Go to app
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<WavyBorder />
|
||||
@ -96,9 +107,6 @@ const Id = () => {
|
||||
<Tabs.Trigger value="deployments">
|
||||
<Link to="deployments">Deployments</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="database">
|
||||
<Link to="database">Database</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="integrations">
|
||||
<Link to="integrations">Integrations</Link>
|
||||
</Tabs.Trigger>
|
||||
|
@ -6,10 +6,14 @@ import {
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@material-tailwind/react';
|
||||
|
||||
import Stepper from '../../../../components/Stepper';
|
||||
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
|
||||
const CreateWithTemplate = () => {
|
||||
@ -44,25 +48,30 @@ 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>
|
||||
<div className="grid grid-cols-3 w-5/6 p-6">
|
||||
<div>
|
||||
<Stepper activeStep={activeStep} stepperValues={stepperValues} />
|
||||
<Steps currentIndex={activeStep} steps={stepperValues} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Outlet context={{ template }} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import templates from 'assets/templates';
|
||||
import RepositoryList from 'components/projects/create/RepositoryList';
|
||||
import { RepositoryList } from 'components/projects/create/RepositoryList';
|
||||
import ConnectAccount from 'components/projects/create/ConnectAccount';
|
||||
import { useOctokit } from 'context/OctokitContext';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
@ -13,8 +13,8 @@ const NewProject = () => {
|
||||
return isAuth ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<Heading as="h3" className="font-medium text-lg">
|
||||
Start with template
|
||||
<Heading as="h3" className="font-medium text-lg pl-1">
|
||||
Start with a template
|
||||
</Heading>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{templates.map((template) => {
|
||||
@ -28,7 +28,7 @@ const NewProject = () => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Heading as="h3" className="font-medium text-lg mt-10">
|
||||
<Heading as="h3" className="font-medium text-lg mt-10 pl-1 mb-3">
|
||||
Import a repository
|
||||
</Heading>
|
||||
<RepositoryList octokit={octokit} />
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -115,8 +115,8 @@ const OverviewTabPanel = () => {
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-[72px]">
|
||||
<div className="col-span-3">
|
||||
<div className="grid grid-cols-5 gap-6 md:gap-[72px]">
|
||||
<div className="col-span-5 md:col-span-3">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Avatar
|
||||
size={48}
|
||||
@ -124,13 +124,13 @@ const OverviewTabPanel = () => {
|
||||
imageSrc={project.icon}
|
||||
type="blue"
|
||||
/>
|
||||
<div className="flex-1 space-y-1">
|
||||
<Heading className="text-lg leading-6 font-medium">
|
||||
<div className="flex-1 space-y-1 overflow-hidden">
|
||||
<Heading className="text-lg leading-6 font-medium truncate">
|
||||
{project.name}
|
||||
</Heading>
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
<p className="text-sm text-elements-low-em tracking-tight truncate">
|
||||
{project.subDomain}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
||||
|
@ -18209,7 +18209,7 @@ use-sync-external-store@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
usehooks-ts@^2.10.0:
|
||||
usehooks-ts@^2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.15.1.tgz#ede348c6f01b4b4fe981e240551624885a2fed83"
|
||||
integrity sha512-AK29ODCt4FT9XleILNbkbjjmkRCNaQrgxQEkvqHjlnT76iPXzTFGvK2Y/s83JEdSxRp43YEnSa3bYBEV6HZ26Q==
|
||||
|
Loading…
Reference in New Issue
Block a user