初めに
今回は簡単な日記アプリを書いていきます。
本記事の内容を利用して発生した損害については、一切の責任を負いません。
ファイルなど:
今回はindex.html一つで書いていきます。
コードのコピペ:
以下のコードをindex.htmlにコピペしてください:
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日記アプリ</title>
<style>
html,
body {
padding: 0;
}
body {
background-color: rgb(46, 46, 46);
color: white;
}
.container {
text-align: center;
color: white;
width: 60%;
height: 50%;
border: 1px solid white;
border-radius: 10px;
margin: 0 auto;
margin-top: 5%;
}
/*ボーダーなしのcontainer*/
.container-main {
color: white;
width: 60%;
height: 50%;
border-radius: 10px;
margin: 0 auto;
margin-top: 5%;
}
/*ボーダーありのtext-alignなし*/
.container-main-card {
border: 1px solid white;
color: white;
width: 60%;
height: 50%;
border-radius: 10px;
margin: 0 auto;
margin-top: 5%;
padding-inline: 10px;
}
input[type="text"],
input[type="password"],
textarea {
background-color: rgb(212, 212, 212);
color: rgb(0, 0, 0);
width: 50%;
height: 30px;
border-radius: 10px;
}
input[type="text"]::placeholder,
input[type="password"]::placeholder,
textarea::placeholder {
color: rgb(0, 0, 0);
}
input[type="text"]:hover,
input[type="password"]:hover,
textarea:hover {
background-color: rgb(163, 163, 163);
}
button {
width: 40%;
height: 30px;
background-color: rgb(78, 78, 78);
color: white;
border: none;
border-radius: 10px;
}
button:hover {
background-color: rgb(31, 31, 31);
border: none;
cursor: pointer;
}
button:disabled,
input:disabled {
cursor: not-allowed;
}
</style>
</head>
<body>
<!--認証画面-->
<div id="login" class="container" hidden>
<h1>パスワード</h1>
<p>パスワードを入力</p>
<input type="password" id="password" placeholder="パスワードを入力">
<br><br><button onclick="login()" id="btn_pass">ログイン</button>
<h4 style="color: red;">注意:このコードを使用して起こった損害に対して一切の責任を負いかねます。</h4>
</div>
<!--パスワード新規発行画面-->
<div id="new_pass" class="container" hidden>
<h1>パスワード</h1>
<p>パスワードが見つかりません。新しく入力してください。</p>
<p>使用できる文字はa~z A~Z 0~9 , .です。</p>
<input type="text" id="password_new" placeholder="新しいパスワードを入力">
<br><br><button id="btn_newpass" onclick="newpass()">パスワードを確認</button>
<h4 style="color: red;">注意:このコードを使用して起こった損害に対して一切の責任を負いかねます。</h4>
</div>
<!--メイン-->
<div class="container-main" id="main" hidden>
<div style="text-align: center;">
<h1>日記一覧</h1>
<p>これまで書いた日記の一覧(新しい順)</p>
<button onclick="jump_to_write()" id="jumpbtn_write">日記を書く</button>
<div id="write" hidden>
<h2>タイトル</h2>
<input type="text" id="title" placeholder="日記のタイトルを入力" >
<h2>内容:</h2>
<textarea id="content" placeholder="日記の内容を入力。(改行などは無視されます。)" style="resize: vertical;"></textarea>
<h3>パスワード(本人確認)</h3>
<input type="password" id="pw" placeholder="本人確認のためにパスワードを入力してください。">
<br><br><button onclick="add_data()" id="save_btn">日記を保存</button>
<br><br><button onclick="hide_write()">隠す</button>
</div>
<br><br><button onclick="show_json()" id="jumpbtn_showJSON">JSONを表示</button>
<div id="JSON_data" style="margin: 0 auto; text-align: center;" hidden>
<textarea id="JSON_data_copy" style="width: 70%; height: 300px; border-radius: 10px; resize: vertical;" readonly></textarea>
<br><br><button onclick="hide_json()">JSONを隠す</button>
</div>
</div>
<div id="memos"></div>
</div>
</body>
<script>
// よく使うdivの一覧と関数によく出てくるdivの一覧:
const new_pass = document.getElementById('new_pass');
const login_pass = document.getElementById('login');
const main = document.getElementById('main');
const memos = document.getElementById('memos');
const JSON_data = document.getElementById('JSON_data');
const data_copy = document.getElementById('JSON_data_copy');
// UUIDを生成(ChatGPTが生成)
function generateUUID() {
// 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' 形式
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0; // 0~15 のランダム整数
const v = c === 'x' ? r : (r & 0x3 | 0x8); // y の部分は 8~b に固定
return v.toString(16);
});
}
// パスワードをハッシュする関数(ChatGPTが生成)
async function tohash(message) {
// 文字列をUint8Arrayに変換
const encoder = new TextEncoder();
const data = encoder.encode(message);
// SHA-256でハッシュ
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// ArrayBufferを16進数文字列に変換
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
// base64にする関数(ChatGPTの出力を参考)
function tobase64(message) {
const Utf8Bytes = new TextEncoder().encode(message);
// バイナリ文字に変更
let binary = '';
Utf8Bytes.forEach(byte => binary += String.fromCharCode(byte));
return btoa(binary);
}
// base64からstringに変換する関数(ChatGPTの出力を参考)
function tostring(message) {
const binary = atob(message);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const decoded = new TextDecoder().decode(bytes);
return decoded;
}
// テキストに含めない文字がないかを確認
function check_text_type(text) {
// 指定された文字列が空白またはa=z,A~Z,0~9,,.以外の文字を使用した際に400(bad request)をreturn
if (!text.trim() || /[^\p{L}0-9,.\s、。!?]/u.test(text.trim())) {
return 400;
} else return 200;
}
function jump_to_write() {
document.getElementById('jumpbtn_write').hidden = true;
document.getElementById('write').hidden = false;
}
function hide_write() {
const title = document.getElementById('title');
const content = document.getElementById('content');
const password = document.getElementById('pw');
title.value = content.value = password.value = '';
document.getElementById('write').hidden = true;
document.getElementById('jumpbtn_write').hidden = false;
}
// ログイン
async function auth(pass) {
if(check_text_type(pass) !== 200) {
alert('パスワードが入力されていない、またはパスワードに使用できない文字を使用しています。');
return;
}
if(await tohash(pass) === localStorage.getItem('key')){
return 200;
} else {
return 401;
}
}
// LocalStorageからデータを取得してStringに戻す
function format_data() {
if(main.hidden) {
alert('エラーが発生しました。');
return;
};
try {
const data = JSON.parse(localStorage.getItem('datas'));
const formated = [];
data.forEach(memo => {
formated.push({
title: tostring(memo.title),
content: tostring(memo.content),
time: tostring(memo.time),
id: tostring(memo.id)
});
});
return formated;
} catch(err) {
alert(`エラーが発生したため、読み込みに失敗しました:\n${err}`);
return null;
}
}
// divにロード
function load_to_disp() {
memos.innerHTML = ``;
const formated = format_data().reverse();
if(!formated) {
return;
}
formated.forEach(memo => {
//要素の作成
const memory = document.createElement('div');
memory.setAttribute('class', 'container-main-card');
memory.innerHTML = `
<h2>${memo.title}</h2>
<p>${memo.content}</p>
<p>${memo.time}</p>
<p>ID: ${memo.id}</p>
<button style="display: block; margin: 0 auto 10px auto;" data-action="delete" data-id="${memo.id}">日記を削除</button>
`;
memos.appendChild(memory);
})
}
// 日記データの保存
async function add_data() {
const title = document.getElementById('title');
const content = document.getElementById('content');
const password = document.getElementById('pw');
const btn = document.getElementById('save_btn');
// 入力の無効化
title.disabled = content.disabled = password.disabled = btn.disabled = true;
// \nなどの除去
content.value = content.value.replace(/[\r\n]+/g, '');
if(check_text_type(title.value) !== 200 || check_text_type(content.value) !== 200) {
alert('タイトル、コンテンツ、パスワードのどれかが空か、どこかに使用できない文字を含んでいます。使用できる文字はa~z A~Z 0~9 , .と ひらがな カタカナ 漢字 、 。 ! ?です。\n(この操作はキャンセルされました。)');
title.disabled = content.disabled = password.disabled = btn.disabled = false;
return;
}
// パスワードチェック
const status = await auth(password.value);
if(status !== 200) {
alert('パスワードが違います。');
title.disabled = content.disabled = password.disabled = btn.disabled = false;
return;
}
if(!confirm(`以下の内容で日記を保存します。よろしいですか?\nタイトル:"${title.value}"\n内容:"${content.value}"`)) {
alert('この操作をキャンセルしました。');
title.disabled = content.disabled = password.disabled = btn.disabled = false;
return;
};
const now = new Date().toLocaleString('ja-JP');
const id = generateUUID();
const new_data = {
title: tobase64(title.value),
content: tobase64(content.value),
time: tobase64(now),
id: tobase64(id)
};
try {
const datas = JSON.parse(localStorage.getItem('datas'));
datas.push(new_data);
localStorage.setItem('datas', JSON.stringify(datas));
alert(`日記を保存しました。`);
title.disabled = content.disabled = password.disabled = btn.disabled = false;
hide_write();
if(!JSON_data.hidden) {
hide_json();
show_json();
}
//情報の更新
load_to_disp();
return;
} catch(err) {
alert(`日記の保存中にエラーが発生したため、操作をキャンセルしました。:\n${err}`);
title.disabled = content.disabled = password.disabled = btn.disabled = false;
return;
}
}
// ログイン
async function login() {
const pass = document.getElementById('password');
const pass_btn = document.getElementById('btn_pass');
pass.disabled = pass_btn.disabled = true;
const status = await auth(pass.value)
if(status !== 200) {
alert('パスワードが違います。');
pass.disabled = pass_btn.disabled = false;
} else {
alert('パスワードが一致しました。');
document.getElementById('login').hidden = true;
document.getElementById('main').hidden = false;
load_to_disp();
}
}
// パスワード(新規作成)
async function newpass() {
//パスワードの取得
const pass = document.getElementById('password_new');
const pass_btn = document.getElementById('btn_newpass');
pass.disabled = pass_btn.disabled = true;
//パスワードが存在するかの確認
if(localStorage.getItem('key')) {
alert('エラー:パスワードがすでに設定されています。');
location.reload(true);
return;
}
// パスワードに使用できない文字が含まれているかの確認:
if (!pass.value.trim() || /[^a-zA-Z0-9]/.test(pass.value.trim())) {
alert('パスワードに使用できない文字があります。');
pass.disabled = pass_btn.disabled = false;
return;
}
// これでいいかの確認:
if(!confirm(`以下のパスワードでよろしいですか?(続行するには「はい」をクリック):\n${pass.value}`)) {
alert('ユーザーによってキャンセルされました。');
pass.disabled = pass_btn.disabled = false;
return;
}
localStorage.setItem('key', await tohash(pass.value));
alert(`パスワードを設定しました:\n${pass.value}(OKを選択すると再読み込みします。)`);
location.reload(true);
}
// 削除イベント(ChatGPTの出力を参考):
document.addEventListener('click', async (e) => {
if(e.target.dataset.action === 'delete') {
const user_input = prompt('続行するにはパスワードを入力:');
const status = await auth(user_input);
if(status !== 200) {
alert('パスワードが一致しなかったため、日記を削除できません。(この操作はキャンセルされました。)');
return;
}
const data_string = localStorage.getItem('datas');
let datas;
try {
datas = JSON.parse(data_string);
} catch {
datas = null
}
if(!datas) {
alert('エラー:データが存在しないか、データが壊れています。');
localStorage.setItem('datas', JSON.stringify([]));
return;
}
// IDの取得とフィルタ
const id = e.target.dataset.id;
const data = datas.filter(memo => tostring(memo.id) === id);
if(data.length !== 1) {
alert('エラー:指定された日記が見つからないか、2件以上ヒットしました。');
return;
}
if(confirm(`以下の日記を削除します。よろしいですか?\nタイトル:"${tostring(data[0].title)}"\n内容:"${tostring(data[0].content)}"\n時間:"${tostring(data[0].time)}"`)) {
const deleted = datas.filter(memo => tostring(memo.id) !== id);
localStorage.setItem('datas', JSON.stringify(deleted));
alert('日記を削除しました。');
if(!JSON_data.hidden) {
hide_json();
show_json();
}
// 情報の更新:
load_to_disp()
return;
}
}
})
// JSONデータをtextAreaに表示
async function show_json() {
document.getElementById('jumpbtn_showJSON').hidden = true;
JSON_data.hidden = false;
const data = JSON.stringify(format_data().map(({id, ...rest}) => rest))
if(data.length < 1) {
data_copy.innerText = '[]';
} else {
data_copy.innerText = data;
}
}
// JSONを隠す
function hide_json() {
data_copy.innerText = ''
JSON_data.hidden = true;
document.getElementById('jumpbtn_showJSON').hidden = false;
}
// ChatGPTの出力を参考
window.onload = async () => {
const datasKey = localStorage.getItem('datas');
// datas が存在しない、または配列でない場合は初期化
let datas;
try {
datas = JSON.parse(datasKey ?? 'null');
if (!Array.isArray(datas)) throw new Error('not array');
} catch {
datas = [];
localStorage.setItem('datas', JSON.stringify(datas));
console.log('datas initialized');
}
// パスワードが未設定なら new_pass 表示
if(!localStorage.getItem('key')) {
new_pass.hidden = false;
} else {
login_pass.hidden = false;
}
};
</script>
</html>
使い方
- index.htmlを開いて、もしパスワードが存在しない場合、パスワードを設定します
- 1.で入力したパスワードを使用してログインします
- 「日記を書く」をクリックして必要なデータを入力したら、「日記を保存」をクリックします
- 内容などに問題がなかったら、確認のダイアログが出るので、「はい」をクリックして保存します
セキュリティなど
- パスワードを設定したうえでそれをSHA-256でハッシュしてLocalStorageに保存してます
- 日記の内容(タイトル、内容、時間、ID)をbase64に変換してから保存してます
最後に
今回は簡単な日記アプリをHTMLとCSSとJavaScriptで書いてみました。ちなみに久々にHTMLとCSSを自分で書いてみました。(もし見づらかったらすみません。)
また、久々にコードを書いたので、もしバグなどがあったらコメントで指摘していただけると助かります!
それではまたお会いしましょう!