feat(domains): DomainCard and WebhookCard styling start (#225)
				
					
				
			### TL;DR Refactored the `DomainCard`, `EditDomainDialog`, and `WebhookCard` components to improve code readability and enhance UI using new shared components like `Tag`, `Heading`, `Button`, and `CustomIcon`. ### What changed? - `DomainCard` component: - Replaced `Chip` with `Tag` component. - Used `Heading`, `Button`, and `CustomIcon` components. - Updated refresh icon to show `LoadingIcon` when checking. - `EditDomainDialog` component: - Used `useToast` hook for toast messages. - `WebhookCard` component: - Used `Input`, `Button`, and `CustomIcon` components for better UI. - Added Storybook stories for the updated components. ### How to test? 1. Go to the project settings page. 2. Verify the `DomainCard` UI updates. 3. Edit a domain and check the toasts. 4. Verify the `WebhookCard` UI and functionality. 5. Run Storybook and inspect the added stories for the components. ### Why make this change? To improve the consistency and user experience of the project settings UI, and to make the components more maintainable by using shared components. ---
This commit is contained in:
		
							parent
							
								
									1b038476c7
								
							
						
					
					
						commit
						9a1c0e8338
					
				| @ -2,7 +2,6 @@ import { useState } from 'react'; | ||||
| import { Domain, DomainStatus, Project } from 'gql-client'; | ||||
| 
 | ||||
| import { | ||||
|   Chip, | ||||
|   Typography, | ||||
|   Menu, | ||||
|   MenuHandler, | ||||
| @ -15,6 +14,15 @@ import EditDomainDialog from './EditDomainDialog'; | ||||
| import { useGQLClient } from 'context/GQLClientContext'; | ||||
| import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog'; | ||||
| import { useToast } from 'components/shared/Toast'; | ||||
| import { Tag } from 'components/shared/Tag'; | ||||
| import { | ||||
|   CheckIcon, | ||||
|   CrossIcon, | ||||
|   GearIcon, | ||||
|   LoadingIcon, | ||||
| } from 'components/shared/CustomIcon'; | ||||
| import { Heading } from 'components/shared/Heading'; | ||||
| import { Button } from 'components/shared/Button'; | ||||
| 
 | ||||
| enum RefreshStatus { | ||||
|   IDLE, | ||||
| @ -79,22 +87,29 @@ const DomainCard = ({ | ||||
|     <> | ||||
|       <div className="flex justify-between py-3"> | ||||
|         <div className="flex justify-start gap-1"> | ||||
|           <Typography variant="h6"> | ||||
|             <i>^</i> {domain.name} | ||||
|           </Typography> | ||||
|           <Chip | ||||
|             className="w-fit capitalize" | ||||
|             value={domain.status} | ||||
|             color={domain.status === DomainStatus.Live ? 'green' : 'orange'} | ||||
|             variant="ghost" | ||||
|             icon={<i>^</i>} | ||||
|           /> | ||||
|           <Heading as="h6" className="flex-col"> | ||||
|             {domain.name}{' '} | ||||
|             <Tag | ||||
|               type={ | ||||
|                 domain.status === DomainStatus.Live ? 'positive' : 'negative' | ||||
|               } | ||||
|               leftIcon={ | ||||
|                 domain.status === DomainStatus.Live ? ( | ||||
|                   <CheckIcon /> | ||||
|                 ) : ( | ||||
|                   <CrossIcon /> | ||||
|                 ) | ||||
|               } | ||||
|             > | ||||
|               {domain.status} | ||||
|             </Tag> | ||||
|           </Heading> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="flex justify-start gap-1"> | ||||
|           <i | ||||
|             id="refresh" | ||||
|             className="cursor-pointer w-8 h-8" | ||||
|             className="cursor-pointer" | ||||
|             onClick={() => { | ||||
|               SetRefreshStatus(RefreshStatus.CHECKING); | ||||
|               setTimeout(() => { | ||||
| @ -102,11 +117,17 @@ const DomainCard = ({ | ||||
|               }, CHECK_FAIL_TIMEOUT); | ||||
|             }} | ||||
|           > | ||||
|             {refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'} | ||||
|             {refreshStatus === RefreshStatus.CHECKING ? ( | ||||
|               <LoadingIcon className="animate-spin" /> | ||||
|             ) : ( | ||||
|               'L' | ||||
|             )} | ||||
|           </i> | ||||
|           <Menu placement="bottom-end"> | ||||
|             <MenuHandler> | ||||
|               <button className="border-2 rounded-full w-8 h-8">...</button> | ||||
|               <Button iconOnly> | ||||
|                 <GearIcon /> | ||||
|               </Button> | ||||
|             </MenuHandler> | ||||
|             <MenuList> | ||||
|               <MenuItem | ||||
| @ -143,13 +164,13 @@ const DomainCard = ({ | ||||
|       {domain.status === DomainStatus.Pending && ( | ||||
|         <Card className="bg-slate-100 p-4 text-sm"> | ||||
|           {refreshStatus === RefreshStatus.IDLE ? ( | ||||
|             <Typography variant="small"> | ||||
|             <Heading> | ||||
|               ^ Add these records to your domain and refresh to check | ||||
|             </Typography> | ||||
|             </Heading> | ||||
|           ) : refreshStatus === RefreshStatus.CHECKING ? ( | ||||
|             <Typography variant="small" className="text-blue-500"> | ||||
|             <Heading className="text-blue-500"> | ||||
|               ^ Checking records for {domain.name} | ||||
|             </Typography> | ||||
|             </Heading> | ||||
|           ) : ( | ||||
|             <div className="flex gap-2 text-red-500 mb-2"> | ||||
|               <div className="grow"> | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import { useCallback, useEffect, useMemo } from 'react'; | ||||
| import { Controller, useForm, SubmitHandler } from 'react-hook-form'; | ||||
| import toast from 'react-hot-toast'; | ||||
| import { Domain } from 'gql-client'; | ||||
| 
 | ||||
| import { | ||||
| @ -9,10 +8,11 @@ import { | ||||
|   Option, | ||||
| } from '@snowballtools/material-tailwind-react-fork'; | ||||
| 
 | ||||
| import { useGQLClient } from '../../../../context/GQLClientContext'; | ||||
| import { useGQLClient } from 'context/GQLClientContext'; | ||||
| import { Modal } from 'components/shared/Modal'; | ||||
| import { Button } from 'components/shared/Button'; | ||||
| import { Input } from 'components/shared/Input'; | ||||
| import { useToast } from 'components/shared/Toast'; | ||||
| 
 | ||||
| const DEFAULT_REDIRECT_OPTIONS = ['none']; | ||||
| 
 | ||||
| @ -40,6 +40,7 @@ const EditDomainDialog = ({ | ||||
|   onUpdate, | ||||
| }: EditDomainDialogProp) => { | ||||
|   const client = useGQLClient(); | ||||
|   const { toast, dismiss } = useToast(); | ||||
| 
 | ||||
|   const getRedirectUrl = (domain: Domain) => { | ||||
|     const redirectDomain = domain.redirectTo; | ||||
| @ -99,10 +100,20 @@ const EditDomainDialog = ({ | ||||
| 
 | ||||
|       if (updateDomain) { | ||||
|         await onUpdate(); | ||||
|         toast.success(`Domain ${domain.name} has been updated`); | ||||
|         toast({ | ||||
|           id: 'domain_id_updated', | ||||
|           title: `Domain ${domain.name} has been updated`, | ||||
|           variant: 'success', | ||||
|           onDismiss: dismiss, | ||||
|         }); | ||||
|       } else { | ||||
|         reset(); | ||||
|         toast.error(`Error updating domain ${domain.name}`); | ||||
|         toast({ | ||||
|           id: 'domain_id_error_update', | ||||
|           title: `Error updating domain ${domain.name}`, | ||||
|           variant: 'error', | ||||
|           onDismiss: dismiss, | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       handleOpen(); | ||||
|  | ||||
| @ -3,6 +3,8 @@ import { useState } from 'react'; | ||||
| import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog'; | ||||
| import { Button } from 'components/shared/Button'; | ||||
| import { useToast } from 'components/shared/Toast'; | ||||
| import { Input } from 'components/shared/Input'; | ||||
| import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon'; | ||||
| 
 | ||||
| interface WebhookCardProps { | ||||
|   webhookUrl: string; | ||||
| @ -14,11 +16,12 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => { | ||||
| 
 | ||||
|   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); | ||||
|   return ( | ||||
|     <div className="flex justify-between w-full mb-3"> | ||||
|       {webhookUrl} | ||||
|     <div className="flex justify-between w-full mb-3 gap-3"> | ||||
|       <Input value={webhookUrl} disabled /> | ||||
|       <div className="flex gap-3"> | ||||
|         <Button | ||||
|           size="sm" | ||||
|           iconOnly | ||||
|           size="md" | ||||
|           onClick={() => { | ||||
|             navigator.clipboard.writeText(webhookUrl); | ||||
|             toast({ | ||||
| @ -29,16 +32,17 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => { | ||||
|             }); | ||||
|           }} | ||||
|         > | ||||
|           Copy | ||||
|           <CopyIcon /> | ||||
|         </Button> | ||||
|         <Button | ||||
|           size="sm" | ||||
|           iconOnly | ||||
|           size="md" | ||||
|           variant="danger" | ||||
|           onClick={() => { | ||||
|             setDeleteDialogOpen(true); | ||||
|           }} | ||||
|         > | ||||
|           X | ||||
|           <TrashIcon /> | ||||
|         </Button> | ||||
|       </div> | ||||
|       <DeleteWebhookDialog | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
|   DomainStatus, | ||||
|   Domain, | ||||
|   Environment, | ||||
|   Permission, | ||||
| } from 'gql-client'; | ||||
| 
 | ||||
| export const user: User = { | ||||
| @ -44,7 +45,7 @@ export const organization: Organization = { | ||||
| export const member: ProjectMember = { | ||||
|   id: '1', | ||||
|   member: user, | ||||
|   permissions: [], | ||||
|   permissions: [Permission.Edit], | ||||
|   isPending: false, | ||||
|   createdAt: '2021-08-01T00:00:00.000Z', | ||||
|   updatedAt: '2021-08-01T00:00:00.000Z', | ||||
| @ -70,7 +71,7 @@ export const environmentVariable1: EnvironmentVariable = { | ||||
| 
 | ||||
| export const domain0: Domain = { | ||||
|   id: '1', | ||||
|   name: 'Domain', | ||||
|   name: 'domain.com', | ||||
|   createdAt: '2021-08-01T00:00:00.000Z', | ||||
|   updatedAt: '2021-08-01T00:00:00.000Z', | ||||
|   branch: 'Branch', | ||||
| @ -78,6 +79,16 @@ export const domain0: Domain = { | ||||
|   redirectTo: null, | ||||
| }; | ||||
| 
 | ||||
| export const domain1: Domain = { | ||||
|   id: '2', | ||||
|   name: 'www.domain.com', | ||||
|   createdAt: '2021-08-01T00:00:00.000Z', | ||||
|   updatedAt: '2021-08-01T00:00:00.000Z', | ||||
|   branch: 'Branch', | ||||
|   status: DomainStatus.Live, | ||||
|   redirectTo: domain0, | ||||
| }; | ||||
| 
 | ||||
| export const deployment0: Deployment = { | ||||
|   id: '1', | ||||
|   url: 'https://deployment.com', | ||||
|  | ||||
| @ -0,0 +1,48 @@ | ||||
| import { StoryObj, Meta } from '@storybook/react'; | ||||
| 
 | ||||
| import DomainCard from 'components/projects/project/settings/DomainCard'; | ||||
| import { domain0, domain1, project } from '../../MockStoriesData'; | ||||
| 
 | ||||
| const meta: Meta<typeof DomainCard> = { | ||||
|   title: 'Project/Settings/DomainCard', | ||||
|   component: DomainCard, | ||||
|   tags: ['autodocs'], | ||||
|   argTypes: { | ||||
|     domains: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     domain: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     branches: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     project: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     onUpdate: { | ||||
|       action: 'update', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default meta; | ||||
| 
 | ||||
| type Story = StoryObj<typeof DomainCard>; | ||||
| 
 | ||||
| export const Default: Story = { | ||||
|   args: { | ||||
|     domains: [domain0, domain1], | ||||
|     domain: domain0, | ||||
|     branches: ['main'], | ||||
|     project: project, | ||||
|   }, | ||||
| }; | ||||
| @ -0,0 +1,43 @@ | ||||
| import { StoryObj, Meta } from '@storybook/react'; | ||||
| 
 | ||||
| import EditDomainDialog from 'components/projects/project/settings/EditDomainDialog'; | ||||
| 
 | ||||
| const meta: Meta<typeof EditDomainDialog> = { | ||||
|   title: 'Components/EditDomainDialog', | ||||
|   component: EditDomainDialog, | ||||
|   tags: ['autodocs'], | ||||
|   argTypes: { | ||||
|     domains: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     open: { | ||||
|       control: { | ||||
|         type: 'boolean', | ||||
|       }, | ||||
|     }, | ||||
|     handleOpen: { | ||||
|       action: 'open', | ||||
|     }, | ||||
|     domain: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     branches: { | ||||
|       control: { | ||||
|         type: 'object', | ||||
|       }, | ||||
|     }, | ||||
|     onUpdate: { | ||||
|       action: 'update', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default meta; | ||||
| 
 | ||||
| type Story = StoryObj<typeof EditDomainDialog>; | ||||
| 
 | ||||
| export const Default: Story = {}; | ||||
| @ -0,0 +1,29 @@ | ||||
| import { Meta, StoryObj } from '@storybook/react'; | ||||
| 
 | ||||
| import WebhookCard from 'components/projects/project/settings/WebhookCard'; | ||||
| 
 | ||||
| const meta: Meta<typeof WebhookCard> = { | ||||
|   title: 'Project/Settings/WebhookCard', | ||||
|   component: WebhookCard, | ||||
|   tags: ['autodocs'], | ||||
|   argTypes: { | ||||
|     webhookUrl: { | ||||
|       control: { | ||||
|         type: 'text', | ||||
|       }, | ||||
|     }, | ||||
|     onDelete: { | ||||
|       action: 'delete', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default meta; | ||||
| 
 | ||||
| type Story = StoryObj<typeof WebhookCard>; | ||||
| 
 | ||||
| export const Default: Story = { | ||||
|   args: { | ||||
|     webhookUrl: 'https://api.retool.com', | ||||
|   }, | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user