今回作成する Webサーバ の構成は,
サーバ
OS : ubuntu 22.04 LTS
Webサーバ : nginx
APサーバ : ruby ( sinatra )
DB : mysql
アプリ : todoList 管理アプリ
開発環境
mac
です。少しずつ機能を追加していく予定です。
今回は version1 で作成した todoList と連動したカレンダーを実装してみます。カレンダーは前月と後月を行き来できるようにし todo リストの期限が到来する日のカレンダーにタイトルを掲載するします。出来上がりの画面は次のような感じです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>todo-schedule</title>
<meta name="description" content="todoList 管理">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="header-block">
<h1>todoList ver2</h1>
</div>
<div id="main-block">
<div id="todo-seg" class="main-block-item">
<div id="input-area">
<input id="todo-input" placeholder="todoを入力" type="text" />
<input id="dead_line-input" placeholder="期限を入力" type="text" />
<button disabled id="add-button">追加</button>
</div>
<h2>未達成 todo 一覧</h2>
<table id="incomplete-area" border="1">
<thead>
<tr>
<th>ID</th>
<th>todo</th>
<th>期限</th>
<th>作成(再登録)日時</th>
<th>操作</th>
</tr>
</thead>
<tbody id="mi-tbl-body">
<tr>
<td>153</td>
<td>appServerが起動していませんよ。</td>
<td>2023-12-31</td> <!-- ここに期限を追加 -->
<td>1975-5-30</td>
<td><button id="comp-btn">達成</button><button id="dlt-btn">削除</button></td>
</tr>
</tbody>
</table>
<h2>達成済 todo 一覧</h2>
<table id="complete-area" border="1">
<thead>
<tr>
<th>ID</th>
<th>todo</th>
<th>期限</th>
<th>完了日時</th>
<th>操作</th>
</tr>
</thead>
<tbody id="zumi-table-body">
<tr>
<td>153</td>
<td>appServerが起動していません。</td>
<td>2023-12-31</td> <!-- ここに期限を追加 -->
<td>1975-5-30</td>
<td><button id="comp-btn">達成</button><button id="dlt-btn">削除</button></td>
</tr>
</tbody>
</table>
</div>
<div id="calendar-seg" class="main-block-item">
<div id="calendar-header">
<button id="prev-month"><</button>
<h2 id="calendar-title">令和5年11月</h2>
<button id="next-month">></button>
</div>
<div id="testdate"></div>
<table height="100px">
<thead>
<tr>
<th>Sun</th>
<th>Mon</th>
<th>Tue</th>
<th>Wed</th>
<th>Thu</th>
<th>Fri</th>
<th>Sat</th>
</tr>
</thead>
<tbody id="calendar-body">
</tbody>
</table>
</div>
</div>
<div id="footer-block">
footer情報
</div>
<p id="message"></p>
<script src="main.js"></script>
</body>
</html>
body {
font-family: sans-serif;
}
input {
border-radius: 16px;
border: none;
padding: 6px 16px;
outline: none;
}
button {
border-radius: 16px;
border: none;
padding: 4px 16px;
}
button:hover {
background-color: #ff7fff;
color: #fff;
cursor: pointer;
}
li {
margin-right: 8px;
}
#input-area {
background-color: #c1ffff;
height: 30px;
border-radius: 8px;
padding: 8px;
margin: 8px;
}
#incomplete-area {
background-color: #c6ffe2;
min-height: 200px;
padding: 8px;
margin: 8px;
border-radius: 8px;
}
#complete-area {
background-color: #ffffe0;
min-height: 200px;
padding: 8px;
margin: 8px;
border-radius: 8px;
}
.title {
text-align: center;
margin-top: 0;
font-weight: bold;
color: #666;
}
#header-block{
text-align: center;
width: 50%;
margin: auto;
border: 1px solid;
padding: 5px;
}
#main-block{
display: flex;
justify-content: center;
border: 1px solid;
padding: 5px;
}
#footer-block{
text-align: center;
border: 1px solid;
padding: 5px;
}
#todo-seg {
background-color: plum;
border: 1px solid;
width: 35%;
}
#calender-seg {
background-color: aqua;
border: 1px solid;
width: 35%;
}
#calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
}
#prev-month , #next-month {
padding: 10px;
background-color: #ccc;
border: none;
cursor: pointer;
}
th , td {
height: 60px;
min-width: 80px;
border:1px solid #333;
}
.prev {
font-size: 0.7em;
color: gray;
}
.next {
font-size: 0.7em;
color: red;
}
.current {
font-size: 0.7em;
color: blue;
}
.category01 {
background-color: red;
border: 1px solid;
}
.category02 {
background-color: blue;
border: 1px solid;
}
.category03 {
background-color: green;
border: 1px solid;
}
const miTableBody = document.getElementById('mi-tbl-body');
const zumiTableBody = document.getElementById('zumi-table-body');
const listTodoInput = document.getElementById('todo-input');
const listDeadLineInput = document.getElementById('dead_line-input');
const listAddButton = document.getElementById('add-button');
const calPrevBtn = document.getElementById('prev-month');
const calNextBtn = document.getElementById('next-month');
const calTitle = document.getElementById('calendar-title');
const calBody = document.getElementById('calendar-body');
// 基準となる日を定めます
let calRefday = new Date();
let caldayFrom = new Date();
let caldayTo = new Date();
function formatDate(createdAt) {
const date = new Date(createdAt);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString();
const day = date.getDate().toString();
return `${year}-${month}-${day}`;
}
function init_todoList() {
miTableBody.innerHTML = '';
zumiTableBody.innerHTML = '';
}
function init_calender() {
let j = 0;
// 画面を初期化します。
calBody.innerHTML = '';
// 基準日が属する月の初日の週の日曜日を取得します。
// getDay() : (0-6), where 0 is Sunday, 1 is Monday, ..., and 6 is Saturday
// getDay() が 0 ( 日曜日 ) だったらそのまま。1 ( 月曜日 ) だったら 1 引く。この要領で必ず日曜日がセットされる。
caldayFrom = new Date( calRefday.getFullYear() , calRefday.getMonth() , 1 - new Date( calRefday.getFullYear() , calRefday.getMonth() , 1 ).getDay() );
let caldayFromString = `${caldayFrom.getFullYear()}-${caldayFrom.getMonth() + 1}-${caldayFrom.getDate()}`;
// 基準日が属する月の末日の週の土曜日を取得します。
caldayTo = new Date( calRefday.getFullYear() , calRefday.getMonth() + 1 , 6 - new Date( calRefday.getFullYear() , calRefday.getMonth() + 1 , 0 ).getDay() );
let caldayToString = `${caldayTo.getFullYear()}-${caldayTo.getMonth() + 1}-${caldayTo.getDate()}`;
let targetDate = new Date( caldayFrom.getFullYear() , caldayFrom.getMonth() , caldayFrom.getDate() );
while( targetDate < caldayTo ) {
const trRowElement = document.createElement('tr');
for( let i = 0 ; i < 7 ; i++ ) {
const tdElement = document.createElement('td');
const divElement = document.createElement( 'div' );
targetDate = new Date( caldayFrom.getFullYear() , caldayFrom.getMonth() , caldayFrom.getDate() + i + j * 7 );
tdElement.id = targetDate.getFullYear() + '-' + ( targetDate.getMonth() + 1 ) + '-' + targetDate.getDate();
divElement.innerText = ( targetDate.getMonth() + 1 ) + '-' + targetDate.getDate();
if( targetDate > new Date( calRefday.getFullYear() , calRefday.getMonth() + 1 , 0 ) ) {
divElement.className = 'next';
}else if( targetDate < new Date( calRefday.getFullYear() , calRefday.getMonth() , 1 ) ) {
divElement.className = 'prev';
}else{
divElement.className = 'current'
}
tdElement.appendChild( divElement );
trRowElement.appendChild( tdElement );
}
calBody.appendChild( trRowElement );
console.log( 'caldayTo :' + caldayTo );
j = j + 1;
}
}
async function rendering() {
// todoList と calender の表示対象期間を取得します。
// データが必要な期間を算出します。
// todoList は対象期間の限定はない ( version2 ) ので全期間を取得
const from_date = '0';
const to_date = '0';
init_todoList();
init_calender();
// 算出された期間のデータを取得します。
// version2 では特に影響なし
const loaded_data = await fetch( '/api/load_data' + `?par1=${from_date}&par2=${to_date}` );
const loaded_data_body = await loaded_data.json();
const loaded_datas = loaded_data_body.datas;
// todoState に応じて mi zumi にデータを振り分けます。
// カレンダー期間に入っていた場合はカレンダーに登録します。
loaded_datas.forEach( ( task ) => {
// todoList を作成します。
const trRowElement = document.createElement('tr');
const tdIdElement = document.createElement('td');
tdIdElement.innerText = task.id;
const tdContentElement = document.createElement('td');
tdContentElement.innerText = task.todo;
const tdDeadLineElement = document.createElement('td');
tdDeadLineElement.innerText = formatDate( task.dead_line );
const tdCreatedAtElement = document.createElement('td');
tdCreatedAtElement.innerText = formatDate( task.createdAt );
const tdBtnAreaElement = document.createElement('td');
const btn1Element = document.createElement('button');
btn1Element.innerText = '戻す';
btn1Element.className = 'und-btn';
btn1Element.addEventListener( 'click', () => {
undoTask(task.id);
})
const btn2Element = document.createElement('button');
btn2Element.innerText = '完了';
btn2Element.className = 'cmp-btn';
btn2Element.addEventListener( 'click', () => {
completeTask(task.id);
})
const btn3Element = document.createElement('button');
btn3Element.innerText = '削除';
btn3Element.className = 'dlt-btn';
btn3Element.addEventListener('click', () => {
deleteTask(task.id);
});
tdBtnAreaElement.appendChild( task.todoState ? btn1Element : btn2Element );
tdBtnAreaElement.appendChild( btn3Element );
trRowElement.appendChild(tdIdElement);
trRowElement.appendChild(tdContentElement);
trRowElement.appendChild(tdDeadLineElement);
trRowElement.appendChild(tdCreatedAtElement);
trRowElement.appendChild(tdBtnAreaElement);
// todoState 0 : 未達成 , 1 : 達成済
if( task.todoState ) {
zumiTableBody.appendChild( trRowElement );
}else{
miTableBody.appendChild( trRowElement );
}
// calender にデータを記述します。
// 表示されているカレンダーに dead_line が入った 未達成todoList をカレンダーに表示します。
const eventDate = new Date( formatDate( task.dead_line ) );
if( eventDate >= caldayFrom && caldayTo >= eventDate && task.todoState == 0 ) {
const cal = document.getElementById( `${new Date(task.dead_line).getFullYear()}-${(new Date(task.dead_line).getMonth())+1}-${new Date(task.dead_line).getDate()}` );
const divElement = document.createElement('div');
divElement.innerText = task.todo;
cal.appendChild( divElement );
}
});
}
async function registerTask() {
const todo = listTodoInput.value;
const deadLine = listDeadLineInput.value;
const requestBody = {
todo,
DeadLine: deadLine,
};
await fetch('/api/list', {
method: 'POST',
body: JSON.stringify(requestBody),
});
listTodoInput.value = '';
listDeadLineInput.value = '';
listAddButton.disabled = true;
await rendering();
}
async function deleteTask(id) {
const requestBody = { id };
await fetch('/api/tasks_delete', {
method: 'POST',
body: JSON.stringify(requestBody),
});
await rendering();
}
async function undoTask(id) {
const requestBody = { id };
await fetch('/api/tasks_undo', {
method: 'POST',
body: JSON.stringify(requestBody),
});
await rendering();
}
async function completeTask(id) {
const requestBody = { id };
await fetch('/api/tasks_complete', {
method: 'POST',
body: JSON.stringify(requestBody),
});
await rendering();
}
function validateInput(event) {
const inputValue = event.target.value;
const isInvalidInput = inputValue.length < 1 || inputValue.length > 30;
listAddButton.disabled = isInvalidInput;
}
function setCalNextMonth( event ) {
calRefday = new Date( calRefday.getFullYear() , calRefday.getMonth() + 1 , calRefday.getDate() );
console.log ( 'setCalNext : ' + calRefday.toLocaleString() );
calTitle.innerText = calRefday.getFullYear() + ' / ' + ( calRefday.getMonth() + 1 ) ;
rendering();
}
function setCalPrevMonth( event ) {
calRefday = new Date( calRefday.getFullYear() , calRefday.getMonth() - 1 , calRefday.getDate() );
console.log ( 'setCalPrev : ' + calRefday.toLocaleString() );
calTitle.innerText = calRefday.getFullYear() + ' / ' + ( calRefday.getMonth() + 1 ) ;
rendering();
}
function main() {
// 具体的なデータをロードする前の準備
// ( 具体的なデータの有無に関わらず設定する必要があるもの )
// 準備1 タスク新規登録ボタンのイベント関連付け
listTodoInput.addEventListener('input', validateInput);
listAddButton.addEventListener('click', registerTask);
// 準備2 カレンダーの表示月を起動した日を含む月に設定
calTitle.innerText = calRefday.getFullYear() + ' / ' + ( calRefday.getMonth() + 1 ) ;
// 準備3 カレンダーの先月次月ボタンのイベント関連付け
calPrevBtn.addEventListener('click', setCalPrevMonth);
calNextBtn.addEventListener('click', setCalNextMonth);
rendering();
}
main();
require 'sinatra'
require 'mysql2'
require 'logger'
post '/api/list' do
request_body = JSON.parse(request.body.read)
todo = request_body['todo']
dead_line = request_body['DeadLine']
insert_task(todo, dead_line)
end
post '/api/tasks_complete' do
request_body = JSON.parse(request.body.read)
id = request_body['id']
update_task_state(id, 1)
end
post '/api/tasks_undo' do
request_body = JSON.parse(request.body.read)
id = request_body['id']
update_task_state(id, 0)
end
post '/api/tasks_delete' do
request_body = JSON.parse(request.body.read)
id = request_body['id']
delete_task(id)
end
def db_client
@db_client ||= Mysql2::Client.new(
:host => "localhost",
:database => "todoList",
:username => "todoList",
:password => 'password',
:connect_timeout => 5
)
end
get '/api/load_data' do
param1 = params['par1']
param2 = params['par2']
client = db_client
# result_set = client.query( "select id, todo, todoState, dead_line, created_at FROM todoList WHERE dead_line between '#{param1} 00:00:00' and '#{param2} 23:59:59' ORDER BY dead_line" )
result_set = client.query( "select id, todo, todoState, dead_line, created_at FROM todoList ORDER BY dead_line" )
logger.info "now called /api/load_data"
logger.info "select id, todo, todoState, dead_line, created_at FROM todoList WHERE dead_line between '#{param1} 00:00:00' and '#{param2} 23:59:59' ORDER BY dead_line"
datas = result_set.map do |row|
{
id: row['id'],
todo: row['todo'],
todoState: row['todoState'],
dead_line: row['dead_line'],
createdAt: row['created_at']
}
end
client.close
{ datas: datas }.to_json
end
def insert_task(todo, dead_line)
client = db_client
statement = client.prepare('INSERT INTO todoList(todo, todoState, dead_line) VALUES (?, 0, ?)')
statement.execute(todo, dead_line)
client.close
end
def update_task_state(id, todo_state)
client = db_client
statement = client.prepare('UPDATE todoList SET todoState = ?, created_at = CURRENT_TIMESTAMP WHERE id = ?')
statement.execute(todo_state, id)
client.close
end
def delete_task(id)
client = db_client
statement = client.prepare('DELETE FROM todoList WHERE id = ?')
statement.execute(id)
client.close
end
def logger
return @logger unless @logger.nil?
# file = File.new("#{settings.root}/log/#{settings.environment}.log", 'a+')
file = File.new( "test.log", 'a+')
file.sync = true
@logger = ::Logger.new( file )
end
logger.info "Hello"
コメント