Skip to content

Commit

Permalink
feat: add new link functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
theodorusclarence committed Jan 18, 2022
1 parent 7bf4782 commit 14ea6ba
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 11 deletions.
18 changes: 9 additions & 9 deletions src/components/forms/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export default function Input({
} = useFormContext();

return (
<div>
<label htmlFor={id} className='block text-sm font-normal text-gray-700'>
<div className='w-full'>
<label htmlFor={id} className='block text-sm font-normal text-gray-200'>
{label}
</label>
<div className='relative mt-1'>
Expand All @@ -59,26 +59,26 @@ export default function Input({
readOnly={readOnly}
className={clsx(
readOnly
? 'bg-gray-100 focus:ring-0 cursor-not-allowed border-gray-300 focus:border-gray-300'
? 'bg-gray-700 focus:ring-0 cursor-not-allowed border-gray-600 focus:border-gray-600'
: errors[id]
? 'focus:ring-red-500 border-red-500 focus:border-red-500'
: 'focus:ring-primary-500 border-gray-300 focus:border-primary-500',
'block w-full rounded-md shadow-sm'
? 'focus:ring-red-400 border-red-400 focus:border-red-400'
: 'focus:ring-primary-500 border-gray-600 focus:border-primary-500',
'bg-dark block w-full text-white rounded-md shadow-sm'
)}
placeholder={placeholder}
aria-describedby={id}
/>

{!hideError && errors[id] && (
<div className='flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none'>
<HiExclamationCircle className='text-xl text-red-500' />
<HiExclamationCircle className='text-xl text-red-400' />
</div>
)}
</div>
<div className='mt-1'>
{helperText && <p className='text-xs text-gray-500'>{helperText}</p>}
{helperText && <p className='text-xs text-gray-300'>{helperText}</p>}
{!hideError && errors[id] && (
<span className='text-sm text-red-500'>{errors[id].message}</span>
<span className='text-sm text-red-400'>{errors[id].message}</span>
)}
</div>
</div>
Expand Down
62 changes: 62 additions & 0 deletions src/lib/notion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,65 @@ export const incrementLinkCount = async (url: Url) => {
},
});
};

export const checkSlugIsTaken = async (slug: string) => {
if (!NOTION_LINK_DATABASE_ID) {
throw new Error('NEXT_PUBLIC_NOTION_LINK_DATABASE_ID env is not defined');
}

const response = await notion.databases.query({
database_id: NOTION_LINK_DATABASE_ID,
filter: {
property: 'slug',
title: {
equals: slug,
},
},
});

return response.results.length > 0;
};

/**
* Add new link to the notion database
*/
export const addLink = async (slug: string, link: string) => {
if (!NOTION_LINK_DATABASE_ID) {
throw new Error('NEXT_PUBLIC_NOTION_LINK_DATABASE_ID env is not defined');
}

await notion.pages.create({
parent: {
database_id: NOTION_LINK_DATABASE_ID,
},
properties: {
slug: {
type: 'title',
title: [
{
type: 'text',
text: { content: slug },
},
],
},
link: {
type: 'rich_text',
rich_text: [
{
type: 'text',
text: { content: link },
},
],
},
count: {
type: 'rich_text',
rich_text: [
{
type: 'text',
text: { content: '0' },
},
],
},
},
});
};
2 changes: 1 addition & 1 deletion src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class MyDocument extends Document {
crossOrigin='anonymous'
/>
</Head>
<body>
<body className='bg-dark text-white'>
<Main />
<NextScript />
</body>
Expand Down
10 changes: 9 additions & 1 deletion src/pages/_middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import { getUrlBySlug } from '@/lib/notion';
export default async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname.split('/')[1];

const whitelist = ['favicons', 'fonts', 'images', 'svg', '', 'testing'];
const whitelist = [
'favicons',
'fonts',
'images',
'svg',
'',
'testing',
'new',
];
if (whitelist.includes(path)) {
return;
}
Expand Down
31 changes: 31 additions & 0 deletions src/pages/api/new.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextApiRequest, NextApiResponse } from 'next';

import { addLink, checkSlugIsTaken } from '@/lib/notion';

export default async function NewLinkHandler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const url = req.body as { link: string; slug: string };

if (!url.link || !url.slug) {
return res.status(400).json({
message: 'Link and slug are required',
});
}

const taken = await checkSlugIsTaken(url.slug);
if (taken) {
return res.status(409).json({
message: 'Slug is already taken',
});
}

await addLink(url.slug, url.link);

res.status(201).send('OK');
} else {
res.status(405).json({ message: 'Method Not Allowed' });
}
}
98 changes: 98 additions & 0 deletions src/pages/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import axios from 'axios';
import * as React from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';

import Accent from '@/components/Accent';
import Button from '@/components/buttons/Button';
import Input from '@/components/forms/Input';
import Layout from '@/components/layout/Layout';
import Seo from '@/components/Seo';

import { DEFAULT_TOAST_MESSAGE } from '@/constant/toast';

type NewLinkFormData = {
slug: string;
link: string;
};

export default function NewLinkPage() {
//#region //*=========== Form ===========
const methods = useForm<NewLinkFormData>({
mode: 'onTouched',
});
const { handleSubmit } = methods;
//#endregion //*======== Form ===========

//#region //*=========== Form Submit ===========
const onSubmit: SubmitHandler<NewLinkFormData> = (data) => {
toast.promise(axios.post('/api/new', data), {
...DEFAULT_TOAST_MESSAGE,
success: 'Link successfully shortened',
});
};
//#endregion //*======== Form Submit ===========

return (
<Layout>
<Seo templateTitle='Shorten!' />

<main>
<section>
<div className='layout flex flex-col items-center py-20 min-h-screen'>
<h1 className='h0'>
<Accent>Shorten New Link</Accent>
</h1>

<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmit)}
className='mt-8 w-full max-w-sm'
>
<div className='space-y-4'>
<Input
id='slug'
label='Slug'
placeholder='slug'
validation={{
required: 'Slug must be filled',
pattern: {
value: /^\S+$/,
message: 'Cannot include whitespace',
},
}}
/>
<Input
id='link'
label='Link'
helperText='Must include http or https'
placeholder='https://google.com'
validation={{
required: 'Link must be filled',
pattern: {
value:
/^(?:https?:\/\/|s?ftps?:\/\/)(?!www | www\.)[A-Za-z0-9_-]+\.+[A-Za-z0-9./%#*&=?_:;-]+$/,
message: 'Please input a valid link',
},
}}
/>
</div>

<div className='flex flex-col mt-5'>
<Button
className='justify-center w-full md:ml-auto md:w-auto'
variant='outline'
isDarkBg
type='submit'
>
Shorten!
</Button>
</div>
</form>
</FormProvider>
</div>
</section>
</main>
</Layout>
);
}

1 comment on commit 14ea6ba

@vercel
Copy link

@vercel vercel bot commented on 14ea6ba Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.