設計情報

投稿者: SPIRERS ナレッジ向上チーム 2022年8月4日 (木)

【SPIRAL 活用例】リアルタイムアンケートシステム

パイプドビッツ(旧社名)社内で、すごいで賞を受賞したリアルタイムアンケートシステムを紹介します。
すごいで賞は、遊び心も含め、会社が盛りあがるような記録を達成した個人または組織に表彰される賞です。
SPIRALで開発したウェビナー視聴画面で、個人を特定してリアルタイムでアンケートを表示する仕組みによって、50%未満だったウェビナーでのアンケート回収率を78%まで引き上げることが出来たとのことです!
設定のサンプルも掲載しているので、ぜひ参考にしてみてください!

リアルタイムアンケートシステムの紹介

SPIRAL ver.1内に専用の視聴画面を作成し、再生されている動画にリアルタイムでアンケートフォームを重ねて表示することができます。
この動画はYoutubeのプレミア公開を利用し、再生の巻き戻しができないようにすることでアンケートの表示時間を設定していきます。
画面イメージ YoutubeのAPIで動画の再生時間を取得して表示しているため、ラグを気にすることなくあらかじめ設定した再生時間にアンケートを表示させることができます。

設定サンプル

DBやマイエリアのタイトル、差替えキーワードなどは設定サンプルと同じでないと動作しません。
変更したい場合は、各ソース内のタイトルや差し替えキーワードの記述を修正する必要があります。
ウェビナー申込者DB
最小限のフィールドで構成したDBの設定例です。
申込者情報やメンテナンス項目、アンケート項目などは必要に応じて追加してください。
基本設定
DBタイプ 通常DB
タイトル webner_member
フィールド
フィールド名 フィールドタイプ 差し替えキーワード 備考
メールアドレス メールアドレス email 入力必須
重複不可
○○に対しての印象を選択してください。 セレクト r_question1
○○導入のご検討状況を選択してください。 セレクト r_question2
本日のセミナーの満足度をお聞かせください。 セレクト r_question3
セレクト r_question○ 必要に応じて追加
マイエリアの作成
ウェビナー申込者DBでマイエリアを作成します。
基本設定
タイトル webnerMyarea
識別キー メールアドレス
パスワード 使用しない
自動発行キー 使用しない
基本設定
使用設定 使用する
ウェビナー視聴ページの作成
クリックログインページ(マイエリア作成時に自動生成されるカスタムページ)にウェビナー視聴ページを作成します。
基本設定
タイトル page_XXXXXX
※ウェビナー視聴ページのタイトルは自由に設定できます。
ページソース
<?php //<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=OFF NAME=XXX --> ?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="shift_jis">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>○○ウェビナー</title>
<style>
* {
  box-sizing: border-box;
}

a {
  text-decoration: none;
}

li {
  list-style: none;
}

body {
  margin: 0;
  background: rgb(130,242,238);
  background: #FFFFFF;
}

header {
  height: 50px;
  background-color: #222222;
}

main {
  display: flex;
}

main .center-area {
  width: 100%;
  padding: 1rem;
  background-color: #FAFAFA;
}

@media (min-width:1200px) {
  main .left-area {
    width: calc( (100% - 1200px) /2 );
  }

  main .center-area {
    width: 1200px;
    padding: 1rem;
    background-color: #FAFAFA;
  }

  main .right-area {
    width: calc( (100% - 1200px) /2 );
  }
}

.movie-box {
  position: relative;
  width: 100%;
}

.movie-basic-info-box {
  width: 100%;
  padding: 1rem 0 .5rem 0;
  border-bottom: 1px solid #CCCCCC;
}

.movie-basic-info-box .title {
  color: #555555;
  font-weight: 400;
  font-size: 1.25rem;
}

.movie-basic-info-box .date {
  margin-bottom: .5rem;
  color: #888888;
  font-weight: normal;
  font-size: .875rem;
}

.movie-basic-info-box .organizer {
  color: #555555;
  font-size: .875rem;
}

.movie-contact-box {
  padding: .5rem;
  border: 1px solid #CCCCCC;
  border-top: 0;
  background-color: #FFFFFF;
  color: #555555;
  font-size: .75rem;
}

.movie-contact-box a {
  display: inline-block;
  margin-left: .5rem;
  padding: .3rem 1rem;
  border: 0;
  border-radius: 3px;
  background-color: #0387B4;
  color: #FFFFFF;
  cursor: pointer;
}

.movie-contact-box a:hover {
  opacity: .7;
}

.movie-notes-box {
  width: 100%;
  padding: 2rem 0 1rem 0;
}

.movie-notes-box .title {
  color: #555555;
  font-weight: bold;
  font-size: .875rem;
}

.movie-notes-box .notes {
  color: #555555;
}

.movie-notes-box .notes ul {
  margin-bottom: 0;
  padding-left: 1rem;
}

.movie-notes-box .notes ul li {
  margin-bottom: 1rem;
  font-size: 1rem;
}

.movie-notes-box .notes ul li:last-child {
  margin-bottom: 0rem;
}

.movie-summary-box {
  padding: 1rem 0 1rem 0;
}

.movie-summary-box .title {
  color: #555555;
  font-weight: bold;
  font-size: .875rem;
}

.movie-summary-box .summary {
  color: #555555;
}

.summary dl {
  margin: 2rem 0 0 0;
}

.summary dl:first-child {
  margin: 1rem 0 0 0;
}

.summary dt {
  font-weight: bold;
  font-size: .875rem;
}

.summary dd {
  margin-left: .5rem;
  font-size: .75rem;
}

.speaker {
  display: flex;
  align-items: start;
  margin-bottom: .5rem;
}

.speaker .role {
  width: 120px;
  padding: 0;
  text-align: right;
}

.speaker .role::after {
  content: ":";
}

.speaker .name {
  margin-left: .15rem;
  padding: 0;
}

.cover {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
  width: 100%;
  height: 100%;
  background-color: rgb(0 0 0 / 80%);
  color: #FFFFFF;
}

.cover .title {
  padding: .25rem 1rem;
  border-bottom: 1px solid #FFFFFF;
  font-weight: bold;
  font-size: 1rem;
}

.cover .enquete-container {
  position: relative;
  width: 100%;
  height: calc(100% - 32px);
}

.enquete-container .enquete {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  width: 100%;
  padding: 0 1rem;
  text-align: left;

  -webkit-transform: translate(-50%,-50%);
}

.enquete .question {
  width: 100%;
  margin-bottom: .25rem;
  padding: 0 .5rem;
  font-size: 1rem;
}

.enquete .question::before {
  content: "Q.";
}

.enquete .choices {
  width: 100%;
  margin-bottom: .25rem;
  padding: .5rem 1rem;
  border-radius: .5rem;
  background-color: rgb(255 255 255 / 30%);
  font-size: .75rem;
}

.choices label {
  display: block;
  margin-bottom: .125rem;
}

.choices input[type="radio"] {
  transform: scale(1.25);
  border-radius: .25rem;
}

.choices input:checked {
  border-color: #3B7DDD;
  background-color: #3B7DDD;
}

.enquete .send {
  width: 100%;
}

.send button {
  display: inline-block;
  padding: .25rem 1rem .25rem 1rem;
  border: 0;
  border-radius: .25rem;
  box-sizing: border-box;
  background-color: #0D6AC3;
  color: #FFFFFF;
  font-size: .75rem;
  cursor: pointer;
}

@media (min-width:600px) {
  .cover .title {
    padding: .25rem 1rem;
  }

  .cover .enquete-container {
    height: calc(100% - 32px);
  }

  .enquete-container .enquete {
    padding: 0 1rem;
  }

  .enquete .question {
    margin-bottom: .25rem;
    font-size: 1.25rem;
  }

  .enquete .choices {
    margin-bottom: .25rem;
    font-size: 1rem;
  }

  .choices input[type="radio"] {
    transform: scale(1.25);
  }

  .send button {
    padding: .25rem 1rem .25rem 1rem;
    font-size: 1rem;
  }
}

@media (min-width:800px) {
  .cover .title {
    padding: 1rem 1rem;
  }

  .cover .enquete-container {
    height: calc(100% - 56px);
  }

  .enquete-container .enquete {
    padding: 0 3rem;
  }

  .enquete .question {
    margin-bottom: 1rem;
    font-size: 1.5rem;
  }

  .enquete .choices {
    margin-bottom: 1rem;
    font-size: 1.5rem;
  }

  .choices input[type="radio"] {
    transform: scale(1.75);
  }

  .send button {
    padding: .25rem 1rem .25rem 1rem;
    font-size: 1.5rem;
  }
}

@media (min-width:1000px) {
  .cover .title {
    padding: 1rem 1rem;
  }

  .cover .enquete-container {
    height: calc(100% - 56px);
  }

  .enquete-container .enquete {
    padding: 0 3rem;
  }

  .enquete .question {
    margin-bottom: 2rem;
    font-size: 2rem;
  }

  .enquete .choices {
    margin-bottom: 2rem;
    font-size: 1.75rem;
  }

  .choices input[type="radio"] {
    transform: scale(2.25);
  }

  .send button {
    padding: .5rem 1.5rem .5rem 1.5rem;
    font-size: 1.75rem;
  }
}
@media (min-width:1200px) {
  .cover .title {
    padding: 1rem 1rem;
  }

  .cover .enquete-container {
    height: calc(100% - 56px);
  }

  .enquete-container .enquete {
    padding: 0 3rem;
  }

  .enquete .question {
    margin-bottom: 2rem;
    font-size: 2.25rem;
  }

  .enquete .choices {
    margin-bottom: 2rem;
    font-size: 2rem;
  }

  .choices input[type="radio"] {
    transform: scale(2.5);
  }

  .send button {
    padding: .5rem 1.5rem .5rem 1.5rem;
    font-size: 2rem;
  }
}

.send button:disabled {
  background-color: #93BBE1;
  color: #FFFFFF;
  cursor: auto;
}

.send button:hover:not(:disabled) {
  background-color: #0F7CE5;
}

footer {
  height: 50px;
  background-color: #222222;
}
</style>
</head>
<body>
<header>

</header>
<main>
  <div class="left-area"></div>
  <div class="center-area">
    <div class="movie-box">
      <div id="player"></div>
      <?php
      function api($_app, $_method, $_params, $api_token = "", $api_token_secret = ""){
        global $SPIRAL;
        if($api_token && $api_token_secret){$SPIRAL->setApiToken($api_token, $api_token_secret);}
        $comm = $SPIRAL->getSpiralApiCommunicator();
        $request = new SpiralApiRequest();
        $request->putAll($_params);
        return $comm->request($_app, $_method, $request);
      }

      $db_title = "webner_member";
      $res = api("database", "get", array("db_title"=>$db_title));
      $fieldList= $res->get("schema")->fieldList;
      foreach($fieldList as $field) {
        if(strpos($field->title,'r_question') !== false) {
          echo "<div class='cover'>\n";
          echo "  <div class='title'>アンケート</div>\n";
          echo "  <div class='enquete-container'>\n";
          echo "    <div class='enquete'>\n";
          echo "      <div class='question'>".$field->name."</div>\n";
          echo "      <div class='choices'>\n";
          $label = $field->label;
          $keywordAry = $label->keywordAry;
          $ids = $label->idAry;
          for($i=0; $i<count($ids); $i++){
          echo "        <label>\n";
          echo "        <input type='radio' name='".$field->title."' value='".$ids[$i]."'>\n";
          echo "      <span>".$keywordAry[$i]."</span>\n";
          echo "        </label>\n";
          }
          echo "      </div>\n";
          echo "      <div class='send'>\n";
          echo "        <button name='send' type='button' disabled>送信</button>\n";
          echo "      </div>\n";
          echo "    </div>\n";
          echo "  </div>\n";
          echo "</div>\n";
        }
      }
      ?>
    </div>
    <div class="movie-contact-box">
      ウェビナーの内容についてのご質問やご相談はこちらからお気軽にお問い合わせください。<a href="お問い合わせフォームなどのURL" target="_blank" rel="noopener noreferrer">お問い合わせ</a>
    </div>
    <div class="movie-basic-info-box">
      <div class="title">新しい開発の選択肢。これからの内製化とは?</div>
      <div class="date">2022年7月6日(水)11:00 ~ 12:00</div>
      <div class="organizer">主催:株式会社パイプドビッツ</div>
    </div>
    <div class="movie-notes-box">
      <div class="title">視聴時の注意</div>
      <div class="notes">
        <ul>
          <li>
            時間になりましたらYoutubeが自動で開始されますが、<u>時間になっても開始されない場合、Youtube画面上の矢印を押してください。</u><br>
          </li>
          <li>
            本動画では視聴中にアンケートが表示されます。<br>
            回答いただくと、本日のウェビナーで使用した資料がDLできます!<br>
          </li>
        </ul>
      </div>
    </div>
  </div>
  <div class="right-area"></div>
</main>
<footer></footer>
    <script>
      // 2. This code loads the IFrame Player API code asynchronously.
      var tag = document.createElement('script');

      tag.src = "https://www.youtube.com/iframe_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

      // 3. This function creates an <iframe> (and YouTube player)
      //    after the API code downloads.
      var player;
      function onYouTubeIframeAPIReady() {
        player = new YT.Player('player', {
          height: '100%',
          width: '100%',
          videoId: 'XXXXXXXXXXX', // Youtubeの動画ID
          playerVars: {
            controls: 0, // コントロールバーを表示しない
            disablekb: 1 // キーボードでの操作をさせない
          },
          events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
          }
        });
      }

      // 4. The API will call this function when the video player is ready.
      function onPlayerReady(event) {
        event.target.playVideo();
      }

      // 5. The API calls this function when the player's state changes.
      //    The function indicates that when playing a video (state=1),
      //    the player should play for six seconds and then stop.
      var done = false;
      var timer1;
      function onPlayerStateChange(event) {
        if (event.data == YT.PlayerState.PLAYING && !done) {
          timer1 = setInterval(timer, 1000);
          done = true;
        }
      }

      function displayOn(number,closeTime) {
        var cover = document.getElementsByClassName("cover")[number - 1];
        cover.style.display = "block";
        setTimeout(function(){cover.style.display = "none";},closeTime);
      }

  function timer(){
    var countdown_s = 0; // YouTubeプレミア公開カウントダウンの秒数(s)
    // アンケートの表示タイミング
    if( Math.floor(player.getCurrentTime() ) == ( (60*0+42) + countdown_s) ){
      displayOn(1,(15*1000));
    }if( Math.floor(player.getCurrentTime() ) == ( (60*1+30) + countdown_s) ){
      displayOn(2,(20*1000));
    }if( Math.floor(player.getCurrentTime() ) == ( (60*10+7) + countdown_s) ){
      displayOn(3,(20*1000));
    }
  }
    </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script>
  // リサイズされたときの処理
  $(window).on('load resize', function() {
    $( ".movie-box" ).height( ( $( ".movie-box" ).width() / 16 ) * 9 );
  });

  // アンケートのいずれかを選択したら、ボタンのdisabledを外す
  $('input[type="radio"]').click(function(){
    $(this).closest(".cover").find('button[name="send"]').removeAttr("disabled");
  });

  // ajaxでアンケートを送信
  $(function(){
    $('.send button').click(function() {
      var fieldTitle = encodeURI( $(this).closest(".cover").find('input[type="radio"]').eq(0).attr('name') );
      var answer = encodeURI( $(this).closest(".cover").find('input[type="radio"]:checked').val() );

      $.ajax({
        type: "POST",
        url: "%url/rel:mpgt:enquete_answ_upd%",
        data: {
          fieldTitle: fieldTitle,
          answer: answer
        },
        success: function(msg){
        }
      });
      $('.cover').hide();
    });
  });

  // セッション維持用
  $(function(){
    setInterval(function(){
      $.ajax({
        type: "POST",
        url: "%url/rel:mpgt:session_upd_pg%",
        success: function(msg){
        }
      });
    },3300000); // セッション維持時間を超えない値を設定(ミリ秒)
  });
  </script>
</body>
</html>
動画IDの設定
ウェビナーの動画のIDを設定します。
// 3. This function creates an <iframe> (and YouTube player)
//    after the API code downloads.
var player;
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    height: '100%',
    width: '100%',
    videoId: 'XXXXXXXXXXX', // Youtubeの動画ID
動画IDは、動画のURLの赤字の箇所となります。
https://www.youtube.com/watch?v=XXXXXXXXXXX
アンケート表示時間の設定
プレミア公開のカウントダウンの秒数とアンケートの表示タイミングを設定します。
function timer(){
  var countdown_s = 0; // YouTubeプレミア公開カウントダウンの秒数(s)
  // アンケートの表示タイミング
  if( Math.floor(player.getCurrentTime() ) == ( (60*0+35) + countdown_s) ){
    displayOn(1,(15*1000));
  }if( Math.floor(player.getCurrentTime() ) == ( (60*1+30) + countdown_s) ){
    displayOn(2,(20*1000));
  }if( Math.floor(player.getCurrentTime() ) == ( (60*10+10) + countdown_s) ){
    displayOn(3,(20*1000));
  }
}
詳細説明
  ︙
}if( Math.floor(player.getCurrentTime() ) == ( (60*1+30) + countdown_s) ){
  displayOn(2,(20*1000));
  ︙
1+30 アンケートを表示する再生時間
(1分30秒)
2 表示するアンケートの番号
(r_question2)
20 アンケートを表示し続ける時間
(20秒)
アンケート回答送信ページの作成
カスタムページを追加して、アンケートの回答結果を送信するためのページを作成します。
基本設定
タイトル enquete_answ_upd
ページソース
<?php //<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=OFF NAME=XXX --> ?>
<?php
function api($_app, $_method, $_params, $api_token = "", $api_token_secret = ""){
  global $SPIRAL;
  if($api_token && $api_token_secret){$SPIRAL->setApiToken($api_token, $api_token_secret);}
  $comm = $SPIRAL->getSpiralApiCommunicator();
  $request = new SpiralApiRequest();
  $request->putAll($_params);
  return $comm->request($_app, $_method, $request);
}

$email = $SPIRAL->getContextByFieldTitle("email");
$field_title = urldecode( $SPIRAL->getParam("fieldTitle") );
$answer = urldecode( $SPIRAL->getParam("answer") );
$res = api("database","update",["db_title"=>"webner_member","search_condition"=>[["name"=>"email","value"=>$email]],"data"=>[ ["name"=>$field_title,"value"=>$answer] ] ]);
?>
セッション維持ページの作成
カスタムページを追加して、マイエリアのセッションを維持するためのページを作成します。
基本設定
タイトル session_upd_pg
ページソース
<?php //<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=OFF NAME=XXX --> ?>
<?php
function api($_app, $_method, $_params, $api_token = "", $api_token_secret = ""){
  global $SPIRAL;
  if($api_token && $api_token_secret){$SPIRAL->setApiToken($api_token, $api_token_secret);}
  $comm = $SPIRAL->getSpiralApiCommunicator();
  $request = new SpiralApiRequest();
  $request->putAll($_params);
  return $comm->request($_app, $_method, $request);
}

$res = api("area","status",["jsessionid"=>$_COOKIE["JSESSIONID"],"my_area_title"=>"webnerMyarea"]);
?>

解決しない場合はこちら コンテンツに関しての
要望はこちら