todoList管理アプリを作ってみる③

nginx - sinatra - mysql

今回作成する 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"

コメント

タイトルとURLをコピーしました