diff --git a/_data/time_table.yml b/_data/time_table.yml new file mode 100644 index 00000000..eb203ccb --- /dev/null +++ b/_data/time_table.yml @@ -0,0 +1,146 @@ +slot_minutes: 10 + +rooms: + - 大会議室 (セッション) + - 展示 (コンテスト用) + - 展示 + - 中会議室 (WS1 電子工作) + - 中会議室 (WS2 LTブース) + - 中会議室 (WS3) + - 中会議室 (WS4) + +room_styles: + 大会議室 (セッション): { color: "#c43b3b" } + 展示 (コンテスト用): { color: "#f5a201" } + 展示: { color: "#888888" } + 中会議室 (WS1 電子工作): { color: "#2e7d32" } + 中会議室 (WS2 LTブース): { color: "#1976d2" } + 中会議室 (WS3): { color: "#7b1fa2" } + 中会議室 (WS4): { color: "#ef6c00" } + +events: + # 大会議室 (セッション) + - room: "大会議室 (セッション)" + start: "10:00" + end: "10:10" + title: "開会式" + + - room: "大会議室 (セッション)" + start: "10:10" + end: "10:20" + title: "基調講演 (宮島さん)" + link: "/sessions/keynote1/" + + - room: "大会議室 (セッション)" + start: "11:00" + end: "12:00" + title: "コンテスト作品発表" + + - room: "大会議室 (セッション)" + start: "12:00" + end: "12:40" + title: "ランチタイム" + note: "適宜お昼をお取りください" + + - room: "大会議室 (セッション)" + start: "12:40" + end: "13:00" + title: "みんなのセッション" + + - room: "大会議室 (セッション)" + start: "13:40" + end: "14:00" + title: "九州チャンピオン座談会" + + - room: "大会議室 (セッション)" + start: "14:20" + end: "14:40" + title: "スピーカーセッション とり子さん (20分)" + + - room: 大会議室 (セッション) + start: "14:40" + end: "14:50" + title: "伸びるかも予備" + + - room: "大会議室 (セッション)" + start: "14:50" + end: "15:10" + title: "スピーカーセッション 早良区Dojoチャンピオン (20分)" + + - room: "大会議室 (セッション)" + start: "15:10" + end: "15:20" + title: "スポンサーセッション (2件)" + + - room: "大会議室 (セッション)" + start: "15:20" + end: "15:30" + title: "招待講演 (小宮山さん)" + link: "/sessions/keynote2/" + + - room: "大会議室 (セッション)" + start: "16:00" + end: "16:10" + title: "コンテスト結果発表" + + - room: "大会議室 (セッション)" + start: "16:10" + end: "16:20" + title: "スポンサーセッション (1件)" + note: "該当なしの場合は CoderDojo Japan 活動報告" + + - room: "大会議室 (セッション)" + start: "16:20" + end: "16:30" + title: "閉会式" + + # 展示 + - room: "展示 (コンテスト用)" + start: "10:00" + end: "16:00" + title: "ファイナリスト作品展示" + note: "14:00 投票締切" + + - room: "展示" + start: "10:00" + end: "16:00" + title: "展示" + + # WS1 電子工作 (随時受付) + - room: "中会議室 (WS1 電子工作)" + start: "11:00" + end: "16:00" + title: "電子工作ワークショップ (随時受付)" + + # WS2 LTブース + - room: "中会議室 (WS2 LTブース)" + start: "11:20" + end: "12:00" + title: "Dojo関係者等大人LT (要申込)" + + - room: "中会議室 (WS2 LTブース)" + start: "13:00" + end: "14:00" + title: "DojoニンジャLT (要申込)" + + # WS3 + - room: "中会議室 (WS3)" + start: "11:00" + end: "12:00" + title: "ブレンダー (要申込)" + + - room: "中会議室 (WS3)" + start: "13:00" + end: "14:00" + title: "PLAYRISE" + + # WS4 + - room: "中会議室 (WS4)" + start: "11:00" + end: "12:00" + title: "PowerPoint とプログラミング" + + - room: "中会議室 (WS4)" + start: "14:10" + end: "15:00" + title: "PowerPoint とプログラミング" diff --git a/_pages/time-table.html b/_pages/time-table.html new file mode 100644 index 00000000..abd65fc6 --- /dev/null +++ b/_pages/time-table.html @@ -0,0 +1,88 @@ +--- +layout: default +permalink: /time-table/ +title: タイムテーブル +--- +{% include navbar.html %} + +{% comment %} + Jekyll プラグインで事前計算されたタイムテーブル表を使用 + ロジックはプラグインで計算済み。Liquid は描画のみを担当 +{% endcomment %} + +{% assign tte = site.data.time_table_events %} +{% assign events = tte.events %} +{% assign rooms = tte.rooms %} +{% assign time_labels = tte.time_labels %} +{% assign total_slots = tte.total_slots %} +{% assign total_rooms = tte.total_rooms %} + +
+

+ Time table + タイムテーブル +

+
+ + + + + {% comment %} ルーム単位でヘッダーを描画 {% endcomment %} + {% for room in rooms %} + + {% endfor %} + + + + + {% comment %} スロット単位(行単位)でイベントを描画 {% endcomment %} + {% for slot in (0..total_slots) %} + + + + {% comment %} 各イベントを描画 {% endcomment %} + {% for room_index in (0..total_rooms) %} + {% assign event = events[slot][room_index] %} + {% assign room = rooms[room_index] %} + + {% if event == 'continued' %} + {% comment %} イベント継続中 (rowspan で描画するため出力は不要) {% endcomment %} + {% elsif event %} + {% comment %} イベントを描画 {% endcomment %} + {% assign accent = event.accent | default: room.style.color | default: '#c43b3b' %} + {% assign link_url = event.url | default: event.link %} + + + {% else %} + {% comment %} イベント無し {% endcomment %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
時間 + {{ room.name }} +
{{ time_labels[slot] }} + {% if link_url %} + + {% else %} + + {% endif %} +
+
+
diff --git a/_plugins/time_table_generator.rb b/_plugins/time_table_generator.rb new file mode 100644 index 00000000..5c13dd18 --- /dev/null +++ b/_plugins/time_table_generator.rb @@ -0,0 +1,100 @@ +module Jekyll + module TimeTableGenerator + # タイムテーブル表を事前に計算してイベント表形式に変換 + # これにより、Liquid テンプレートは単純な表示のみを担当 + class Generator < Jekyll::Generator + safe true + priority :high + + # デフォルト設定値 + DEFAULT_SLOT_MINUTES = 15 + DEFAULT_DAY_START_HOUR = 10 # 10:00 + DEFAULT_DAY_END_HOUR = 16 # 16:00 + + def generate(site) + tt = site.data['time_table'] + return unless tt + + # 設定値を取得 + slot_minutes = tt.fetch('slot_minutes', DEFAULT_SLOT_MINUTES) + day_start = tt.fetch('day_start_hour', DEFAULT_DAY_START_HOUR) + day_end = tt.fetch('day_end_hour', DEFAULT_DAY_END_HOUR) + rooms = tt.fetch('rooms', []) + events = tt.fetch('events', []) + room_styles = tt.fetch('room_styles', {}) + + # イベント情報を表形式で生成 + time_table_events = create_event_table(events, rooms, room_styles, slot_minutes, day_start, day_end) + + # 生成したイベント表データを Liquid に提供 + site.data['time_table_events'] = time_table_events + end + + private + + def create_event_table(events, rooms, room_styles, slot_minutes, day_start, day_end) + total_slots = ((day_end - day_start) * 60 / slot_minutes).to_i + + # 時間ラベルを生成 + time_labels = (0...total_slots).map do |slot| + minutes = day_start * 60 + slot * slot_minutes + "#{minutes / 60}:%02d" % (minutes % 60) + end + + # ルーム情報を生成(room.style でアクセス可能) + rooms_data = rooms.map do |room_name| + { + 'name' => room_name, + 'style' => room_styles[room_name] || {} + } + end + + # イベント表を生成(2次元配列) + table = Array.new(total_slots) { Array.new(rooms.size) } + + events.each do |event| + place_event(event, table, rooms, slot_minutes, day_start, total_slots) + end + + { + 'events' => table, + 'rooms' => rooms_data, + 'time_labels' => time_labels, + 'total_slots' => total_slots - 1, # Liquidの (0..n) は inclusive なので -1 + 'total_rooms' => rooms.size - 1, # Liquidの (0..n) は inclusive なので -1 + } + end + + def place_event(event, table, rooms, slot_minutes, day_start, total_slots) + room_index = rooms.index(event['room']) + return unless room_index + + # 時間を分に変換して揃える + event_start = time_to_minutes(event['start']) + event_end = time_to_minutes(event['end']) + + # スロット計算(分に揃える) + slot_start = [(event_start - day_start * 60) / slot_minutes, 0].max.to_i + slot_end = [(event_end - day_start * 60) / slot_minutes, total_slots].min.to_i + duration = slot_end - slot_start + + return if slot_start >= total_slots || duration <= 0 + + # イベントの長さ情報 (duration) を追加 + table[slot_start][room_index] = event.merge('duration' => duration) + + # 継続スロットをマーク + (slot_start + 1...slot_end).each do |slot| + break if slot >= total_slots + table[slot][room_index] = 'continued' + end + end + + def time_to_minutes(time_str) + return 0 unless time_str + hours, minutes = time_str.split(':').map(&:to_i) + hours * 60 + minutes + end + end + end +end diff --git a/_sass/pages/time-table.scss b/_sass/pages/time-table.scss new file mode 100644 index 00000000..5b29d230 --- /dev/null +++ b/_sass/pages/time-table.scss @@ -0,0 +1,111 @@ +/* ====== スクロール容器 ====== */ +.ttable-wrap{ + position: relative; + width: 100%; + max-width: 100%; + overflow-x: auto; + overflow-y: visible; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; + contain: content; + scrollbar-gutter: stable both-edges; +} + +/* ====== テーブル ====== + PCでも「時間は4桁」「会場は全文表示」できるように + table-layout: auto と最小幅制約を組み合わせる +*/ +.ttable{ + table-layout: fixed; + --w-start: 5rem; + --room-min: clamp(235px, calc((100dvw - var(--w-start) - 10rem)), 20rem); + --row-h: 56px; + + width: calc(var(--w-start) + var(--room-min) * var(--room-count)); +} + +/* ヘッダー */ +.ttable caption{ + text-align: left; + font-weight: 700; + padding: 10px 12px; +} +.ttable thead th{ + background: #fafafa; + border-bottom: 1px solid #e6e6e9; + padding: 10px 12px; + font-weight: 800; + text-align: left; + white-space: nowrap; +} +.ttable__th--start{ left: 0; min-width: var(--w-start); } +.ttable thead th.ttable__th--start{ position: sticky; z-index: 4; } + +/* 会場ヘッダーと本文セルの最小幅をそろえる(PCで潰れない) */ +.ttable__th--room{ border-left: 1px solid #ececf1; color:#111; background: + linear-gradient(0deg, rgba(255,255,255,0.88), rgba(255,255,255,0.88)), var(--room-color, #c43b3b); + width: var(--room-min); +} +.ttable tbody td{ width: var(--room-min); } + +/* 行ストライプ & グリッド線 */ +.ttable tbody tr{ height: var(--row-h); } +.ttable tbody tr:nth-child(odd){ background: #fff; } +.ttable tbody tr:nth-child(even){ background: #fcfcfe; } +.ttable tbody tr{ + background-image: linear-gradient(to right, rgba(0,0,0,0.10) 50%, rgba(0,0,0,0) 0); + background-size: 6px 1px; /* 破線のピッチ/太さ */ + background-repeat: repeat-x; + background-position: left bottom; +} + +/* セル共通 */ +.ttable__cell{ + padding: 8px 10px; + border-right: 1px solid #f0f0f3; + vertical-align: top; + /* 固定高さを外し、rowspanで正しく縦に伸びるようにする */ + height: auto; + background-clip: padding-box; +} + +/* 左1列(sticky) */ +.ttable__cell--start{ + position: sticky; z-index: 2; background: #fff; left: 0; + min-width: var(--w-start); font-weight: 700; text-align: left; +} +/* stickyの左列は行の背景の上に乗るため、 + 横点線が隠れないよう同じ罫線を重ねる */ +.ttable__cell--start{ + background-image: linear-gradient(to right, rgba(0,0,0,0.10) 50%, rgba(0,0,0,0) 0); + background-size: 6px 1px; + background-repeat: repeat-x; + background-position: left bottom; +} + +/* 空き枠 */ +.ttable__cell--empty{ background: #fff; } + +/* イベント:カード化(CEATEC風) */ +.ttable__cell--event{ background: #fff; } +.ttable__cell--event .ttable__event{ min-height: calc(var(--row-h) * var(--span, 1)); } +.ttable__event{ + position: relative; + border: 1px solid #e5e6eb; + border-radius: 14px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + padding: 10px 12px 12px; + background: #fff; + height: 100%; + display: flex; + flex-direction: column; + overflow: clip; + text-decoration: none; + color: inherit; +} +.ttable__event[href]{ cursor: pointer; } +.ttable__event::before{ content:""; position:absolute; inset:0 0 auto 0; height: 6px; background: var(--accent, #c43b3b); } +.ttable__event-time{ font-weight: 800; font-size: 1.05rem; letter-spacing: .3px; margin: 8px 0 4px; color: #c43b3b; } +.ttable__event-title{ font-weight: 800; line-height: 1.35; margin-bottom: 4px; color: #121212; } +.ttable__event-subtitle{ color: #ee7d05; font-weight: 700; font-size: .92rem; line-height: 1.3; } +.ttable__badge{ position: absolute; top: 8px; right: 10px; padding: 2px 8px; border-radius: 999px; font-size: .85rem; font-weight: 800; color: #fff; background: var(--accent,#c43b3b); box-shadow: 0 1px 4px rgba(0,0,0,.1); } diff --git a/_tests/custom_checks.rb b/_tests/custom_checks.rb index ff381efa..1fd70829 100644 --- a/_tests/custom_checks.rb +++ b/_tests/custom_checks.rb @@ -3,6 +3,7 @@ require 'json' require 'yaml' +require 'time' class CustomChecks < ::HTMLProofer::Check BASE_PATH = '_site' @@ -10,7 +11,9 @@ class CustomChecks < ::HTMLProofer::Check def run filename = @runner.current_filename puts "\tchecking ... " + filename.delete_prefix('_site') + check_directory_structure(filename) if filename.end_with?('.html') + check_time_table_overlaps if filename.end_with?('time-table/index.html') end # Check directory structure to ensure all pages are generated as index.html @@ -45,4 +48,48 @@ def is_redirect_file?(filename) content.include?(" e + add_failure("タイムテーブル検証エラー: #{e.message}") + end end diff --git a/css/main.scss b/css/main.scss index 0609d487..b3c16371 100644 --- a/css/main.scss +++ b/css/main.scss @@ -8,3 +8,4 @@ @use 'includes/post'; @use 'components/staff'; +@use 'pages/time-table';