Skip to content

Commit

Permalink
feat: adds ability to add multiple domains at once.
Browse files Browse the repository at this point in the history
  • Loading branch information
towfiqi committed Jan 15, 2024
1 parent 8b0ee56 commit faa88c9
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 57 deletions.
58 changes: 32 additions & 26 deletions components/domains/AddDomain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,57 @@ import { useAddDomain } from '../../services/domains';
import { isValidDomain } from '../../utils/client/validators';

type AddDomainProps = {
domains: DomainType[],
closeModal: Function
}

const AddDomain = ({ closeModal }: AddDomainProps) => {
const AddDomain = ({ closeModal, domains = [] }: AddDomainProps) => {
const [newDomain, setNewDomain] = useState<string>('');
const [newDomainError, setNewDomainError] = useState<boolean>(false);
const [newDomainError, setNewDomainError] = useState('');
const { mutate: addMutate, isLoading: isAdding } = useAddDomain(() => closeModal());

const addDomain = () => {
// console.log('ADD NEW DOMAIN', newDomain);
if (isValidDomain(newDomain.trim())) {
setNewDomainError(false);
setNewDomainError('');
const existingDomains = domains.map((d) => d.domain);
const insertedDomains = newDomain.split('\n');
const domainsTobeAdded:string[] = [];
const invalidDomains:string[] = [];
insertedDomains.forEach((dom) => {
const domain = dom.trim();
if (isValidDomain(domain)) {
if (!existingDomains.includes(domain)) {
domainsTobeAdded.push(domain);
}
} else {
invalidDomains.push(domain);
}
});
if (invalidDomains.length > 0) {
setNewDomainError(`Please Insert Valid Domain names. Invalid Domains: ${invalidDomains.join(', ')}`);
} else if (domainsTobeAdded.length > 0) {
// TODO: Domain Action
addMutate(newDomain.trim());
} else {
setNewDomainError(true);
addMutate(domainsTobeAdded);
}
};

const handleDomainInput = (e:React.FormEvent<HTMLInputElement>) => {
if (e.currentTarget.value === '' && newDomainError) { setNewDomainError(false); }
const handleDomainInput = (e:React.ChangeEvent<HTMLTextAreaElement>) => {
if (e.currentTarget.value === '' && newDomainError) { setNewDomainError(''); }
setNewDomain(e.currentTarget.value);
};

return (
<Modal closeModal={() => { closeModal(false); }} title={'Add New Domain'}>
<div data-testid="adddomain_modal">
<h4 className='text-sm mt-4'>
Domain Name {newDomainError && <span className=' ml-2 block float-right text-red-500 text-xs font-semibold'>Not a Valid Domain</span>}
</h4>
<input
className={`w-full p-2 border border-gray-200 rounded mt-2 mb-3 focus:outline-none focus:border-blue-200
${newDomainError ? ' border-red-400 focus:border-red-400' : ''} `}
type="text"
<h4 className='text-sm mt-4'>Domain Names</h4>
<textarea
className={`w-full h-40 border rounded border-gray-200 p-4 outline-none
focus:border-indigo-300 ${newDomainError ? ' border-red-400 focus:border-red-400' : ''}`}
placeholder="Type or Paste Domains here. Insert Each Domain in a New line."
value={newDomain}
placeholder={'example.com'}
onChange={handleDomainInput}
autoFocus={true}
onKeyDown={(e) => {
if (e.code === 'Enter') {
e.preventDefault();
addDomain();
}
}}
/>
onChange={handleDomainInput}>
</textarea>
{newDomainError && <div><span className=' ml-2 block float-right text-red-500 text-xs font-semibold'>{newDomainError}</span></div>}
<div className='mt-6 text-right text-sm font-semibold'>
<button className='py-2 px-5 rounded cursor-pointer bg-indigo-50 text-slate-500 mr-3' onClick={() => closeModal(false)}>Cancel</button>
<button className='py-2 px-5 rounded cursor-pointer bg-blue-700 text-white' onClick={() => !isAdding && addDomain() }>
Expand Down
40 changes: 23 additions & 17 deletions pages/api/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type DomainsGetRes = {
}

type DomainsAddResponse = {
domain: Domain|null,
domains: DomainType[]|null,
error?: string|null,
}

Expand Down Expand Up @@ -59,23 +59,29 @@ export const getDomains = async (req: NextApiRequest, res: NextApiResponse<Domai
}
};

export const addDomain = async (req: NextApiRequest, res: NextApiResponse<DomainsAddResponse>) => {
if (!req.body.domain) {
return res.status(400).json({ domain: null, error: 'Error Adding Domain.' });
}
const { domain } = req.body || {};
const domainData = {
domain: domain.trim(),
slug: domain.trim().replaceAll('-', '_').replaceAll('.', '-'),
lastUpdated: new Date().toJSON(),
added: new Date().toJSON(),
};
const addDomain = async (req: NextApiRequest, res: NextApiResponse<DomainsAddResponse>) => {
const { domains } = req.body;
if (domains && Array.isArray(domains) && domains.length > 0) {
const domainsToAdd: any = [];

try {
const addedDomain = await Domain.create(domainData);
return res.status(201).json({ domain: addedDomain });
} catch (error) {
return res.status(400).json({ domain: null, error: 'Error Adding Domain.' });
domains.forEach((domain: string) => {
domainsToAdd.push({
domain: domain.trim(),
slug: domain.trim().replaceAll('-', '_').replaceAll('.', '-'),
lastUpdated: new Date().toJSON(),
added: new Date().toJSON(),
});
});
try {
const newDomains:Domain[] = await Domain.bulkCreate(domainsToAdd);
const formattedDomains = newDomains.map((el) => el.get({ plain: true }));
return res.status(201).json({ domains: formattedDomains });
} catch (error) {
console.log('[ERROR] Adding New Domain ', error);
return res.status(400).json({ domains: [], error: 'Error Adding Domain.' });
}
} else {
return res.status(400).json({ domains: [], error: 'Necessary data missing.' });
}
};

Expand Down
4 changes: 2 additions & 2 deletions pages/domain/[slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const SingleDomain: NextPage = () => {
const activDomain: DomainType|null = useMemo(() => {
let active:DomainType|null = null;
if (domainsData?.domains && router.query?.slug) {
active = domainsData.domains.find((x:DomainType) => x.slug === router.query.slug);
active = domainsData.domains.find((x:DomainType) => x.slug === router.query.slug) || null;
}
return active;
}, [router.query.slug, domainsData]);
Expand Down Expand Up @@ -86,7 +86,7 @@ const SingleDomain: NextPage = () => {
</div>

<CSSTransition in={showAddDomain} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
<AddDomain closeModal={() => setShowAddDomain(false)} />
<AddDomain closeModal={() => setShowAddDomain(false)} domains={domainsData?.domains || []} />
</CSSTransition>

<CSSTransition in={showDomainSettings} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
Expand Down
4 changes: 2 additions & 2 deletions pages/domain/console/[slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const DiscoverPage: NextPage = () => {
const activDomain: DomainType|null = useMemo(() => {
let active:DomainType|null = null;
if (domainsData?.domains && router.query?.slug) {
active = domainsData.domains.find((x:DomainType) => x.slug === router.query.slug);
active = domainsData.domains.find((x:DomainType) => x.slug === router.query.slug) || null;
}
return active;
}, [router.query.slug, domainsData]);
Expand Down Expand Up @@ -71,7 +71,7 @@ const DiscoverPage: NextPage = () => {
</div>

<CSSTransition in={showAddDomain} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
<AddDomain closeModal={() => setShowAddDomain(false)} />
<AddDomain closeModal={() => setShowAddDomain(false)} domains={domainsData?.domains || []} />
</CSSTransition>

<CSSTransition in={showDomainSettings} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
Expand Down
4 changes: 2 additions & 2 deletions pages/domain/insight/[slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const InsightPage: NextPage = () => {
const activDomain: DomainType|null = useMemo(() => {
let active:DomainType|null = null;
if (domainsData?.domains && router.query?.slug) {
active = domainsData.domains.find((x:DomainType) => x.slug === router.query.slug);
active = domainsData.domains.find((x:DomainType) => x.slug === router.query.slug) || null;
}
return active;
}, [router.query.slug, domainsData]);
Expand Down Expand Up @@ -71,7 +71,7 @@ const InsightPage: NextPage = () => {
</div>

<CSSTransition in={showAddDomain} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
<AddDomain closeModal={() => setShowAddDomain(false)} />
<AddDomain closeModal={() => setShowAddDomain(false)} domains={domainsData?.domains || []} />
</CSSTransition>

<CSSTransition in={showDomainSettings} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
Expand Down
2 changes: 1 addition & 1 deletion pages/domains/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const Domains: NextPage = () => {
</div>

<CSSTransition in={showAddDomain} timeout={300} classNames="modal_anim" unmountOnExit mountOnEnter>
<AddDomain closeModal={() => setShowAddDomain(false)} />
<AddDomain closeModal={() => setShowAddDomain(false)} domains={domainsData?.domains || []} />
</CSSTransition>
<CSSTransition in={showSettings} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
<Settings closeSettings={() => setShowSettings(false)} />
Expand Down
15 changes: 8 additions & 7 deletions services/domains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type UpdatePayload = {
domain: DomainType
}

export async function fetchDomains(router: NextRouter, withStats:boolean) {
export async function fetchDomains(router: NextRouter, withStats:boolean): Promise<{domains: DomainType[]}> {
const res = await fetch(`${window.location.origin}/api/domains${withStats ? '?withstats=true' : ''}`, { method: 'GET' });
if (res.status >= 400 && res.status < 600) {
if (res.status === 401) {
Expand Down Expand Up @@ -56,9 +56,9 @@ export function useFetchDomains(router: NextRouter, withStats:boolean = false) {
export function useAddDomain(onSuccess:Function) {
const router = useRouter();
const queryClient = useQueryClient();
return useMutation(async (domainName:string) => {
return useMutation(async (domains:string[]) => {
const headers = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
const fetchOpts = { method: 'POST', headers, body: JSON.stringify({ domain: domainName }) };
const fetchOpts = { method: 'POST', headers, body: JSON.stringify({ domains }) };
const res = await fetch(`${window.location.origin}/api/domains`, fetchOpts);
if (res.status >= 400 && res.status < 600) {
throw new Error('Bad response from server');
Expand All @@ -67,11 +67,12 @@ export function useAddDomain(onSuccess:Function) {
}, {
onSuccess: async (data) => {
console.log('Domain Added!!!', data);
const newDomain:DomainType = data.domain;
toast(`${newDomain.domain} Added Successfully!`, { icon: '✔️' });
const newDomain:DomainType[] = data.domains;
const singleDomain = newDomain.length === 1;
toast(`${singleDomain ? newDomain[0].domain : `${newDomain.length} domains`} Added Successfully!`, { icon: '✔️' });
onSuccess(false);
if (newDomain && newDomain.slug) {
router.push(`/domain/${data.domain.slug}`);
if (singleDomain) {
router.push(`/domain/${newDomain[0].slug}`);
}
queryClient.invalidateQueries(['domains']);
},
Expand Down

0 comments on commit faa88c9

Please sign in to comment.