短縮リンク作成サービスを開発した話 - フロントエンド編02

## 概要
2023年末頃に、短縮リンク作成サービスを開発・リリースしました。
開発したサービスは以下のものになります。
https://nlia.jp
この記事は前回の続きです。↓前回の記事
https://qiita.com/yatsumi823/items/ce679ce835ccf3ea4a91

前回は、トップページ作成の部分までの内容でした。今回は、リンク作成画面についてです。
この記事では、実際のコードも交えながら、どのような仕組みで動いているのかを解説します。

## ライブラリ
このページで使用するライブラリは、React-hook-formと、Chakra UIです。それぞれのライブラリについての詳細は前回の記事を参照してください。

## パス
前回の記事までと同様、Pages Routerを使用しています。リンク作成ページのパスは、以下の通りです。
/src/pages/shortenLink/index.tsx

## コンポーネントの分け方
コンポーネントは、以下の3種類に分けています。

- ヘッダー（ShortenLinkHeader）
- リンク作成フォーム（MakeShortenLinkForm）
- フッター（ShortenLinkFooter）

## ページ全体のコード
ページ全体のコードは以下の通りです。とはいっても、各コンポーネントを組み合わせるだけのコードになります。
```tsx
import Head from 'next/head'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import { Box, VStack } from '@chakra-ui/react'
import { ShortenLinkFooter } from '@/components/ShortenLinkFooter'
import { MakeShortenLinkForm } from '@/components/MakeShortenLinkForm'
import { ShortenLinkHeader } from '@/components/ShortenLinkHeader'

const inter = Inter({ subsets: ['latin'] })

export default function Home() {
  return (
    <>
      <Head>
        <title>短縮リンク作成 - N-LIA</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <header>
        <ShortenLinkHeader />
      </header>
      <main className={`${styles.main} ${inter.className}`}>
        <Box textAlign={'center'}>
          <VStack>
            <MakeShortenLinkForm />
          </VStack>
        </Box>
      </main>
      <footer>
            <ShortenLinkFooter />
      </footer>
    </>
  )
}

```

## ヘッダー
ヘッダーには、「戻るボタン」「タイトル」「N-LIAトップページへのリンク」の3つの要素を追加します。
ヘッダーのパスは以下の通りです。
/src/components/ShortenLinkHeader.tsx

### 戻るボタン
「戻る」ボタンは、Next Routerの「router.back()」を用いて実装します。
```tsx
// 必要なものをインポート
import router from 'next/router';
import {
    Button,
    Text,
} from '@chakra-ui/react';
import { ArrowBackIcon } from '@chakra-ui/icons'

// 「戻る」ボタン
<Button aria-label="戻る" variant="contained" onClick={() => router.back()}>
    <ArrowBackIcon /><Text ms={2}>戻る</Text>
</Button>
```

### タイトル
タイトルは、Headingで文字を表示します。
```tsx
// 必要なものをインポート
import {
    Heading,
} from '@chakra-ui/react';

// タイトル
<Heading as='h1' fontSize="1xl">
    短縮リンク
</Heading>
```

### N-LIAトップページへのリンク
Next Linkで実装します。

```tsx
// 必要なものをインポート
import NextLink from "next/link";

// リンク
<NextLink href="/" passHref>
    <Heading as='h1' fontSize="2xl" cursor="pointer">
        N-LIA
    </Heading>
</NextLink>
```

### 最終的なコード
最終的なコードは以下の通りです。

```tsx
import {
    Box,
    Flex,
    Container,
    Heading,
    Button,
    Text,
} from '@chakra-ui/react';
import { ArrowBackIcon } from '@chakra-ui/icons'
import NextLink from "next/link";
import router from 'next/router';
import { FC } from 'react';

export const ShortenLinkHeader: FC = () => {    
    return (
        <Box px={4} borderBottomColor={'brand.300'} borderStyle={'solid'} borderColor={'brand.300'} borderWidth={'1px'} position={'fixed'} width={'100%'} zIndex={999} display={'flex'} background={'white'}>
            <Container maxW="container.lg">
                <Flex py="2" justifyContent="space-between" alignItems="center">
                    <Button aria-label="戻る" variant="contained" onClick={() => router.back()}>
                        <ArrowBackIcon /><Text ms={2}>戻る</Text>
                    </Button>
                        <Heading as='h1' fontSize="1xl">
                            短縮リンク
                        </Heading>
                    <NextLink href="/" passHref>
                        <Heading as='h1' fontSize="2xl" cursor="pointer">
                            N-LIA
                        </Heading>
                    </NextLink>
                </Flex>
            </Container>
        </Box>
    );
}
```

## リンク作成フォーム
メインの部分です。リンクの入力欄、リンク作成ボタン、短縮リンクの表示エリア、コピーボタンを追加します。
（QRコードの表示については今回は省略します）
リンク作成フォームのパスは以下の通りです。
/src/components/MakeShortenLinkForm.tsx

### リンクの入力欄

```tsx
// 必要なものをインポート
import {
    FormControl,
    FormLabel,
    Input,
    FormErrorMessage,
} from '@chakra-ui/react'

// 表示部分
return (
    <form>
        <FormControl>
            <FormLabel>URLを入力</FormLabel>
            <Input type='url' id='beforeLink'  />
            <FormErrorMessage>
                エラーメッセージを表示
            </FormErrorMessage>
        </FormControl>
    </form>
    );
```

### リンク作成ボタン
リンク作成ボタンは以下の通りです。
```tsx
// 必要なものをインポート
import {
    FormControl,
    Button,
} from '@chakra-ui/react'

// 表示部分
return (
    <FormControl mb={7}>
        <Button type="submit">短縮リンクを作成</Button>
    </FormControl>
    );
```

### 短縮リンクの表示エリア
短縮リンクの表示エリアは以下の通りです。
```tsx
// 必要なものをインポート
import {
    FormControl,
    Button,
} from '@chakra-ui/react'

const [copyText, setCopyText] = useState("");

// 表示部分
return (
    <FormControl>
        <FormLabel>短縮後のリンク</FormLabel>
        <InputGroup size='md'>
            <Input type='url' id='afterLink' value={shortenUrl} onChange={(event) => setCopyText(event.target.value)} readOnly/>
        </InputGroup>
        <FormHelperText textAlign={'left'} ms={4}>短縮後のリンクです。</FormHelperText>
    </FormControl>
    );
```

### コピーボタン
```tsx
// 必要なものをインポート
import {
    FormControl,
    Button,
} from '@chakra-ui/react'

const copyUrlClick = () => copyUrl()
function copyUrl(){
    navigator.clipboard.writeText(copyText);
}

// 表示部分
return (
    <InputRightElement width='4.5rem'>
        <Button h='1.75rem' size='sm' onClick={copyUrlClick}>
            コピー
        </Button>
    </InputRightElement>
    );
```

### フォームのバリデーション
URL入力欄のバリデーションです。
```tsx
// React-hook
import { SubmitHandler, useForm } from "react-hook-form"
// フォーム定義
type Input = {
     url: string
};

// フォーム定義
const {
    register,
    handleSubmit,
    formState: {
        errors ,
    },
} = useForm<Input>();

// フォーム送信ボタンを押された時の処理
const onsubmit: SubmitHandler<Input> = (data) => {
    makeShortenUrl(data.url);
}

// ベースのURL
const baseUrl = 'nlia.jp/';

// 短縮リンク作成関数
function makeShortenUrl(_longUrl:string){
    let apiResponse = createShortenLink(_longUrl).then( // リンク作成APIに、入力されたURLを投げる
        function(responseValue){
            let newUrl = baseUrl + responseValue.data.urlToken;
            setShortenUrl(newUrl);
            setCopyText('https://' + newUrl);
        }
    );
}

// 表示部分
return (
    <form onSubmit={handleSubmit(onsubmit)}>
        <FormControl isInvalid={Boolean(errors.url)} mb={7}>
            <FormLabel>URLを入力</FormLabel>
            <Input type='url' id='beforeLink'  
            {...register('url', { 
                required: "URLは必須です。",
                pattern: {
                    value: /https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#\u3000-\u30FE\u4E00-\u9FA0\uFF01-\uFFE3]+/g,
                    message: "urlの形式で入力してください。"
                }
            })}/>
            <FormErrorMessage>
                {errors.url && errors.url.message}
            </FormErrorMessage>
        </FormControl>
        <FormControl mb={7}>
            <Button type="submit">短縮リンクを作成</Button>
        </FormControl>
    </form>
);
```

### 全体のコード
```tsx
// Chakra UI コンポーネント
import {
    FormControl,
    FormLabel,
    FormHelperText,
    Box,
    Button,
    Input,
    InputGroup,
    InputRightElement,
    FormErrorMessage,
  } from '@chakra-ui/react'

// reactFC, useState
import { FC, useState  } from 'react';

// リンク作成API
import { createShortenLink } from '@/services/api';

// React-hook
import { SubmitHandler, useForm } from "react-hook-form"

// QRコードコンポーネント
import ShowQRCode from './ShowQRCode';

// フォーム定義
type Input = {
     url: string
};

export const MakeShortenLinkForm: FC = () => {
   // useForm初期化
    const {
        register,
        handleSubmit,
        formState: {
            errors ,
            },
    } = useForm<Input>();

    // フォーム送信ボタンを押された時の処理
    const onsubmit: SubmitHandler<Input> = (data) => {
        makeShortenUrl(data.url);
    }
    
    const [shortenUrl, setShortenUrl] = useState("");
    const baseUrl = 'nlia.jp/';

    const [copyText, setCopyText] = useState("");
    const copyUrlClick = () => copyUrl()
    function copyUrl(){
        navigator.clipboard.writeText(copyText);
    }

    function makeShortenUrl(_longUrl:string){
        let apiResponse = createShortenLink(_longUrl).then(
            function(responseValue){
                let newUrl = baseUrl + responseValue.data.urlToken;
                setShortenUrl(newUrl);
                setCopyText('https://' + newUrl);
            }
        );
    }

    return (
        <Box>
            <form onSubmit={handleSubmit(onsubmit)}>
                <FormControl isInvalid={Boolean(errors.url)} mb={7}>
                    <FormLabel>URLを入力</FormLabel>
                    <Input type='url' id='beforeLink'  
                    {...register('url', { 
                        required: "URLは必須です。",
                        pattern: {
                            value: /https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#\u3000-\u30FE\u4E00-\u9FA0\uFF01-\uFFE3]+/g,
                            message: "urlの形式で入力してください。"
                        }
                    })}/>
                    <FormErrorMessage>
                        {errors.url && errors.url.message}
                    </FormErrorMessage>
                </FormControl>
                <FormControl mb={7}>
                    <Button type="submit">短縮リンクを作成</Button>
                </FormControl>
                <FormControl>
                    <FormLabel>短縮後のリンク</FormLabel>
                    <InputGroup size='md'>
                        <Input type='url' id='afterLink' value={shortenUrl} onChange={(event) => setCopyText(event.target.value)} readOnly/>
                        <InputRightElement width='4.5rem'>
                            <Button h='1.75rem' size='sm' onClick={copyUrlClick}>
                                コピー
                            </Button>
                        </InputRightElement>
                    </InputGroup>
                    <FormHelperText textAlign={'left'} ms={4}>短縮後のリンクです。</FormHelperText>
                </FormControl>
                <Box display='flex' justifyContent='center' alignItems='center' mt={4}>
                    {shortenUrl.length > 0 &&
                        <ShowQRCode url={shortenUrl} />
                    }
                </Box>
            </form>
        </Box>
    );
}

```

## フッター
ヘッダーには、「ホームボタン」「リリースノートボタン」の2つの要素を追加します。
ヘッダーのパスは以下の通りです。
/src/components/ShortenLinkFooter.tsx

### 「ホーム」ボタン
「ホーム」ボタンは、NextLinkで実装します。
```tsx
// 必要なものをインポート
import NextLink from "next/link";
import { IoHomeOutline } from "react-icons/io5";
import {
    Button,
} from '@chakra-ui/react';

// 表示部分
return (
    <NextLink href="/shortenLink" passHref>
        <Button aria-label="ホーム" variant="contained">
            <IoHomeOutline />
        </Button>
    </NextLink>
    );
```

### 「リリースノート」ボタン
「リリースノート」ボタンは、NextLinkで実装します。
```tsx
// 必要なものをインポート
import NextLink from "next/link";
import { IoHomeOutline } from "react-icons/io5";
import {
    Button,
} from '@chakra-ui/react';

// 表示部分
return (
    <NextLink href="/shortenLink/releaseNote" passHref>
        <Button aria-label="リリースノート" variant="contained">
            <CiStickyNote /><Text ms={2}>v1.2.0</Text>
        </Button>
    </NextLink>
    );
```

### 全体のコード
```tsx
import {
    Box,
    Flex,
    Container,
    Button,
    Text,
} from '@chakra-ui/react';
import NextLink from "next/link";
import { FC } from 'react';
import { IoHomeOutline } from "react-icons/io5";
import { CiStickyNote } from "react-icons/ci";

export const ShortenLinkFooter: FC = () => {    
    return (
        <Box px={4} borderBottomColor={'brand.300'} borderStyle={'solid'} borderColor={'brand.300'} borderWidth={'1px'} position={'fixed'} width={'100%'} bottom={0} zIndex={10} background={'white'} textAlign={'center'}>
            <Container maxW="container.lg">
                <Flex py="2" justifyContent="space-between" alignItems="center">
                    <NextLink href="/shortenLink" passHref>
                        <Button aria-label="ホーム" variant="contained">
                            <IoHomeOutline />
                        </Button>
                    </NextLink>
                    <NextLink href="/shortenLink/releaseNote" passHref>
                        <Button aria-label="リリースノート" variant="contained">
                            <CiStickyNote /><Text ms={2}>v1.2.0</Text>
                        </Button>
                    </NextLink>
                </Flex>
            </Container>
        </Box>
    );
}
```

## まとめ
少し長くなりましたが、今回はリンク作成フォームの実装についてでした。次回は、APIとリリースノートページについてです。