0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HexabaseとNext.js、blastengineを使ってお問い合わせフォームを作る

Posted at

Hexabaseはエンタープライズ企業向けのBaaS(Backend as a Service)を提供しています。認証やデータストア(データベース)、ファイルストレージ、FaaS(Function as a Service)といった機能があります。

Untitled.png

今回はそんなHexabaseとblastengineを使ってお問い合わせフォームを実装します。

コード

今回のコードはhttps://github.com/blastengineMania/hexabase-nextjs-contact にアップしてあります。実装時の参考にしてください。

アーキテクチャ

今回のアーキテクチャは以下のようになります。Hexabaseのデータストアにお問い合わせ内容を登録した際に、それをフックとしてアクションスクリプト(Hexbaseの提供するFaaS)を呼び出します。その内容をblastengineに送ることで、お問い合わせ主への確認メールや自分たちへの通知を行います。

Untitled 1.png

blastengineの準備

ユーザ登録する

blastengineにユーザ登録します。管理画面に入るためのユーザID、パスワードが手に入るので、ログインします(ユーザIDは後で使います)。

Untitled.png

送信元ドメインのSPFを設定する

送信元として利用するドメイン(自分で持っているもの)の設定をします。これは任意のドメイン管理サービスで設定できますが、TXTレコードに以下のSPFを追加します。

txt @ v=spf1 include:spf.besender.jp ~all

APIキーを取得する

ログイン後、管理画面の右上にある設定メニューに移動します。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/47b2b2ac-2e41-77fe-d84f-0ae0b7541f36.jpeg

そして設定の中で、APIキーを取得します。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/2b5ca124-3020-1d39-84e4-911cd4db27e2.png

Hexabaseの準備

まずHexabase側の準備です。以下を順番に作成します。

  • ワークスペース
    任意の名前でOKです。ワークスペースIDはメモしておきます。
  • プロジェクト
    任意の名前でOKです。プロジェクトIDはメモしておきます。
  • データストア
    日本語名「お問い合わせ」、英語名はContactとします。データストアIDもContactとします。

データストアの設計

お問い合わせテーブルは、以下の項目を持っています。

項目 英語名&ID
会社名 テキスト company
名前 テキスト name
メールアドレス テキスト mailaddress
問い合わせ種別 選択 type
問い合わせ内容 テキスト body

Untitled 2.png

選択肢の用意

問い合わせ種別の選択肢を、以下のように作成します。英語名とIDは同じにしておきます。

日本語名 英語名&ID
会社について Company
製品について Product
採用について Recruitment
PR&広報 PR

Untitled 3.png

パブリックトークンの準備

Hexabaseでは認証して利用するのが基本ですが、お問い合わせの場合は認証は使わないでしょう。そこで、特定のユーザー情報に紐付くパブリックトークンを生成します。なお、執筆現在ではワークスペースごとの設定が必要です(要問い合わせ)。

Untitled 4.png

実装について

フロントエンド

フロントエンドはNext.jsで実装します。入力項目は以下の通りです。これは先ほどのデータストアの設計と合わせています。

項目 変数名
会社名 テキスト company
名前 テキスト name
メールアドレス テキスト mailaddress
問い合わせ種別 選択 type
問い合わせ内容 テキスト body

Untitled 1.jpeg

コードは以下のようになります。

ステートの準備

今回は以下のステートを用意しています。

// フォームの型
type formData = {
  company: string;
  name: string;
  email: string;
  type: string;
  message: string;
};

// 選択項目の型
type SelectOption = {
  label: string;
  value: string;
};

// フォームのデフォルト値
const defaultValue: formData = {
	company: '',
	name: '',
	email: '',
	type: '',
	message: '',
};
const [form, setForm] = useState<formData>(defaultValue);
const [options, setOptions] = useState<SelectOption[]>([]);
const [project, setProject] = useState<Project>(null);

表示されたタイミングの処理

表示された際に、選択肢「問い合わせ種別」を作成します。

useEffect(() => {
	initialize();
}, []);

const initialize = async () => {
	// パブリックトークンをセット
	await client.setToken(process.env.NEXT_PUBLIC_PUBLIC_TOKEN!);
	// 利用するワークスペースをセット
	await client.setWorkspace(process.env.NEXT_PUBLIC_WORKSPACE_ID!);
	// 利用するプロジェクトをセット
	const project = await client.currentWorkspace.project(
		process.env.NEXT_PUBLIC_PROJECT_ID!
	);
	// プロジェクトはuseStateを使って更新
	setProject(project);
	// 利用するデータストアをセット
	const datastore = await project.datastore(
		process.env.NEXT_PUBLIC_DATASTORE_ID!
	);
	// フィールドデータを取得
	const field = await datastore.field('type');
	// フィールドデータから選択肢を取得
	const options = await field.options();
	// フォームの初期値をセット
	setForm({ ...form, ...{ type: options[0].id } });
	// 選択肢をセット
	setOptions(
		options.map((option) => {
			return {
				value: option.id,
				label: option.value.ja,
			};
		})
	);
};

これで options を使って選択肢を表示できます。

<div className="block type">
	<label htmlFor="frm-type">お問い合わせ種別</label>
	<select
		onChange={(e) =>
			setForm({ ...form, ...{ type: e.target.value } })
		}
	>
		{options.map((option) => (
			<option value={option.value}>{option.label}</option>
		))}
	</select>
</div>

フォームについて

フォームは以下のようになります。入力されたら、その値をフォームに反映しています。ボタンやフォームの送信時には send 関数を呼んでいます。

<>
	<div className={styles.container}>
		<Head>
			<title>お問い合わせ</title>
		</Head>

		<main className={styles.main}>
			<h3 className={styles.title}>お問い合わせ</h3>
			<form className="container" onSubmit={send}>
				<div className="company block">
					<label htmlFor="frm-company">会社名</label>
					<input
						id="frm-company"
						type="text"
						value={form.company}
						autoComplete="company"
						onChange={(e) =>
							setForm({ ...form, ...{ company: e.target.value } })
						}
						required
					/>
				</div>
				<div className="account block">
					<label htmlFor="frm-name">名前</label>
					<input
						id="frm-name"
						type="text"
						value={form.name}
						autoComplete="name"
						onChange={(e) =>
							setForm({ ...form, ...{ name: e.target.value } })
						}
						required
					/>
				</div>
				<div className="email block">
					<label htmlFor="frm-email">メールアドレス</label>
					<input
						id="frm-email"
						type="email"
						value={form.email}
						autoComplete="email"
						onChange={(e) =>
							setForm({ ...form, ...{ email: e.target.value } })
						}
						required
					/>
				</div>
				<div className="block type">
					<label htmlFor="frm-type">お問い合わせ種別</label>
					<select
						onChange={(e) =>
							setForm({ ...form, ...{ type: e.target.value } })
						}
					>
						{options.map((option) => (
							<option value={option.value}>{option.label}</option>
						))}
					</select>
				</div>
				<div className="message block">
					<label htmlFor="frm-message">Message</label>
					<textarea
						id="frm-message"
						rows={6}
						value={form.message}
						onChange={(e) =>
							setForm({ ...form, ...{ message: e.target.value } })
						}
					>
					</textarea>
				</div>
				<button onClick={send}>送信</button>
			</form>
		</main>
	</div>
</>

また、CSS(global.css)は以下のようになります。

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  background: #1e1e1e;
  min-height: 100vh;
  display: flex;
  color: rgb(243, 241, 239);
  justify-content: center;
  align-items: center;
}

.block {
  display: flex;
  flex-direction: column;
}

.name {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}

.container {
  font-size: 1.3rem;
  border-radius: 10px;
  width: 85%;
  padding: 50px;
  box-shadow: 0 54px 55px rgb(78 78 78 / 25%), 0 -12px 30px rgb(78 78 78 / 25%),
    0 4px 6px rgb(78 78 78 / 25%), 0 12px 13px rgb(78 78 78 / 25%),
    0 -3px 5px rgb(78 78 78 / 25%);
}

.container input {
  font-size: 1.2rem;
  margin: 10px 0 10px 0px;
  border-color: rgb(31, 28, 28);
  padding: 10px;
  border-radius: 5px;
  background-color: #e8f0fe;
}

.container select {
  font-size: 1.2rem;
  margin: 10px 0 10px 0px;
  border-color: rgb(31, 28, 28);
  padding: 10px;
  border-radius: 5px;
  background-color: #e8f0fe;
}

.container textarea {
  margin: 10px 0 10px 0px;
  padding: 5px;
  border-color: rgb(31, 28, 28);
  border-radius: 5px;
  background-color: #e8f0fe;
  font-size: 20px;
}

.container h1 {
  text-align: center;
  font-weight: 600;
}

.name div {
  display: flex;
  flex-direction: column;
}

.block button {
  padding: 10px;
  font-size: 20px;
  width: 30%;
  border: 3px solid black;
  border-radius: 5px;
}

.button {
  display: flex;
  align-items: center;
}

textarea {
  resize: none;
}

Untitled.jpeg

送信した際の処理

送信ボタンを押した際の処理として、Hexabaseにレコードを保存します。これはHexabaseのTypeScript SDKを使って実装しています。

const send = async (e) => {
	e.preventDefault();
	// お問い合わせが入るテーブル
	const datastore = await project.datastore(
		process.env.NEXT_PUBLIC_DATASTORE_ID!
	);
	// お問い合わせ種別のフィールドを取得
	const field = await datastore.field('type');
	// お問い合わせの選択肢を取得
	const options = await field.options();
	// 該当する選択肢を取得
	const option = options.find((option) => option.id === form.type);
	// レコードを作成
	const item = await datastore.item();
	// レコードにフォームの入力値を適用して、保存
	await item
		.set('company', form.company)
		.set('name', form.name)
		.set('type', option.id)
		.set('email', form.email)
		.set('message', form.message)
		.save();
	// 送信完了のアラート
	alert('送信しました');
	// 入力値を初期化
	setForm(defaultValue);
};

Untitled 5.png

アクションスクリプト

データストアへの保存処理ができたので、保存をトリガーとしたアクションスクリプトを作成します。まず、blastengineに関連する環境設定を2つ追加します。これはアプリケーション設定のプログラム拡張より行います。

環境設定名
BLASTENGINE_USER_ID 先ほど作成したAPIユーザー名
BLASTENGINE_API_KEY 先ほど作成したAPIキー

Untitled 1.jpeg

データストアのアクションスクリプトを追加

続けて、新規保存処理時のアクションスクリプトを追加します。これはHexabaseの管理画面、データストアの設定画面で作成します。

Untitled 6.png

実際のコードは以下のようになります。このコードは ポスト (データ保存後)の処理として実行します。

コードの基本

Hexabaseのアクションスクリプトでは、main関数が呼ばれます。つまり、以下のようになります。この時、レコードデータが引数として渡されます。

async function main(params) {

}

今回は以下のようにし、 sendByBlastengine 関数を呼んでいます。

async function main(params) {
    try {
        const res = await sendByBlastengine(params);
        logger.log(res);
        return res;
    } catch (err) {
        logger.log(err);
    }
}

async function sendByBlastengine(params) {
}

以下は sendByBlastengine 関数内の実装です。

トークンを生成する

先ほど環境変数に指定したAPIユーザー名、APIキーを使ってトークンを生成します。このトークンはHTTPリクエストのヘッダーに対して、利用します。

const apiKey = '{BLASTENGINE_API_KEY}';
const apiUser = '{BLASTENGINE_USER_ID}';
const str = `${apiUser}${apiKey}`;
const hashHex = crypto
	.createHash('sha256')
	.update(str, 'utf8')
	.digest('hex');
const token = Buffer
	.from(hashHex.toLowerCase())
	.toString('base64');
const headers = {
		Authorization: `Bearer ${token}`,
	'Content-Type': 'application/json',
};

件名・本文を用意する

続けてメール送信する内容を準備します。

const text_part = `${params.fields_data.name.value}様

お問い合わせいただきありがとうございます。内容を確認し、追ってご連絡いたします。

会社名:
${params.fields_data.company.value}
お名前:
${params.fields_data.name.value}
お問い合わせ内容:
${params.fields_data.message.value}
`;
const data = {
from: {
	email: 'no-reply@blastengine.jp',
	name: '管理者'
},
to: params.fields_data.email.value || 'info@blastengine.jp',
subject: 'お問い合わせありがとうございます',
encode: 'UTF-8',
text_part,
};

最後にネットワークライブラリの axios を使って、blastengineにメール送信を行います。

return axios.post('<https://app.engn.jp/api/v1/deliveries/transaction>', data, { headers });

スクリプト全体は以下のようになります。

async function main(params) {
    try {
        const res = await sendByBlastengine(params);
        logger.log(res);
        return res;
    } catch (err) {
        logger.log(err);
    }
}

async function sendByBlastengine(params) {
    const apiKey = '{BLASTENGINE_API_KEY}';
    const apiUser = '{BLASTENGINE_USER_ID}';
    const str = `${apiUser}${apiKey}`;
    const hashHex = crypto
    	.createHash('sha256')
    	.update(str, 'utf8')
    	.digest('hex');
    const token = Buffer
    	.from(hashHex.toLowerCase())
    	.toString('base64');

    const headers = {
        Authorization: `Bearer ${token}`,
    	'Content-Type': 'application/json',
    };

    const text_part = `${params.fields_data.name.value}様

    お問い合わせいただきありがとうございます。内容を確認し、追ってご連絡いたします。

    会社名:
    ${params.fields_data.company.value}
    お名前:
    ${params.fields_data.name.value}
    お問い合わせ内容:
    ${params.fields_data.message.value}
    `;
    const data = {
		from: {
			email: 'no-reply@blastengine.jp',
			name: '管理者'
		},
		to: params.fields_data.email.value || 'info@blastengine.jp',
		subject: 'お問い合わせありがとうございます',
		encode: 'UTF-8',
		text_part,
	};

    return axios.post('<https://app.engn.jp/api/v1/deliveries/transaction>', data, { headers });
};

まとめ

今回はHexabaseとblastengine、Next.jsを使ってお問い合わせフォームを作成しました。入力項目の選択肢を含め、Hexabase側で管理できるので修正も容易です。

今回はメール本文を直書きしていますが、Hexabaseのデータストアに保存しておいても良いでしょう。その場合は、アクションスクリプト側でデータストアから取得して利用することになります。

エンジニア向けメール配信システム「ブラストエンジン(blastengine)」

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?