掲示板実装
This commit is contained in:
27
app/views/boards/_post.html.erb
Normal file
27
app/views/boards/_post.html.erb
Normal file
@@ -0,0 +1,27 @@
|
||||
<% is_me = post.participant == current_participant %>
|
||||
<div class="flex <%= is_me ? 'justify-end' : 'justify-start' %> mb-4 message-item" id="post_<%= post.id %>">
|
||||
<% unless is_me %>
|
||||
<div class="flex-shrink-0 mr-3">
|
||||
<!-- アバター代わりの国名バッジ -->
|
||||
<span class="inline-flex items-center justify-center h-8 w-8 rounded-full text-xs font-bold border border-gray-300 bg-white text-gray-700 shadow-sm" title="<%= post.participant.power %>">
|
||||
<%= post.participant.power ? post.participant.power[0..1] : '?' %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-lg <%= is_me ? 'order-1' : 'order-2' %>">
|
||||
<div class="flex items-baseline space-x-2 mb-1 <%= is_me ? 'justify-end' : 'justify-start' %>">
|
||||
<% unless is_me %>
|
||||
<span class="text-xs font-bold text-gray-900"><%= post.participant.power %></span>
|
||||
<% end %>
|
||||
<span class="text-xs text-gray-500"><%= l post.created_at, format: :short %></span>
|
||||
<% if post.phase.present? %>
|
||||
<span class="text-xs bg-gray-100 px-1 rounded text-gray-600 border border-gray-200"><%= post.phase %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-2 rounded-lg shadow-sm text-sm <%= is_me ? 'bg-indigo-600 text-white rounded-br-none' : 'bg-white border border-gray-200 text-gray-900 rounded-bl-none' %>">
|
||||
<%= simple_format(h(post.body), {}, wrapper_tag: "div") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
194
app/views/boards/index.html.erb
Normal file
194
app/views/boards/index.html.erb
Normal file
@@ -0,0 +1,194 @@
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<%= render "games/header", game: @game, display_turn: @game.latest_turn, current_participant: @current_participant, hide_controls: true %>
|
||||
|
||||
<div class="md:flex md:items-center md:justify-between mb-6">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
||||
外交・交渉掲示板
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
他国との秘密交渉や全体へのアナウンスを行います。
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 flex md:mt-0 md:ml-4">
|
||||
<%= link_to game_path(@game), class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %>
|
||||
<i class="fa-solid fa-map mr-2"></i> マップに戻る
|
||||
<% end %>
|
||||
|
||||
<% if @game.status == 'in_progress' %>
|
||||
<%= link_to new_game_board_path(@game), class: "ml-3 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %>
|
||||
<i class="fa-solid fa-plus mr-2"></i> 新しい掲示板を作成
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @game.status == 'finished' %>
|
||||
<div class="mb-6 bg-yellow-50 border-l-4 border-yellow-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-history text-yellow-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700">
|
||||
このゲームは終了しています。すべての掲示板は履歴モード(閲覧専用)です。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 左カラム: 掲示板一覧 -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
|
||||
<!-- 掲示板リスト -->
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% if @boards.empty? %>
|
||||
<li class="px-4 py-4 sm:px-6 text-center text-gray-500 text-sm">
|
||||
掲示板がありません
|
||||
</li>
|
||||
<% else %>
|
||||
<% @boards.each do |board| %>
|
||||
<li>
|
||||
<%= link_to game_board_path(@game, board), class: "block hover:bg-gray-50" do %>
|
||||
<div class="px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<% if board.global? %>
|
||||
<span class="flex-shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 mr-3">
|
||||
<i class="fa-solid fa-earth-americas mr-1"></i> 共通
|
||||
</span>
|
||||
<p class="text-sm font-medium text-indigo-600 truncate">
|
||||
Global Board (全体掲示板)
|
||||
</p>
|
||||
<% else %>
|
||||
<span class="flex-shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mr-3">
|
||||
<i class="fa-solid fa-handshake mr-1"></i> 交渉
|
||||
</span>
|
||||
<p class="text-sm font-medium text-indigo-600 truncate">
|
||||
<% members = board.participants.map(&:power).compact.sort %>
|
||||
<%= members.join(' / ') %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if board.is_public? %>
|
||||
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<i class="fa-solid fa-bullhorn mr-1"></i> 公開中
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="ml-2 flex-shrink-0 flex">
|
||||
<% unread_count = board.unread_count_for(@current_participant) %>
|
||||
<% if unread_count > 0 %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
<%= unread_count %> 未読
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 sm:flex sm:justify-between">
|
||||
<div class="sm:flex">
|
||||
<p class="flex items-center text-sm text-gray-500">
|
||||
<% last_post = board.board_posts.last %>
|
||||
<% if last_post %>
|
||||
<i class="fa-solid fa-comment-dots flex-shrink-0 mr-1.5 text-gray-400"></i>
|
||||
<span class="truncate max-w-xs"><%= last_post.body.truncate(30) %></span>
|
||||
<span class="ml-2 text-xs text-gray-400">- <%= last_post.participant.power %></span>
|
||||
<% else %>
|
||||
<span class="text-gray-400">投稿なし</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
||||
<% if last_post %>
|
||||
<i class="fa-regular fa-clock flex-shrink-0 mr-1.5 text-gray-400"></i>
|
||||
<p>
|
||||
<%= time_ago_in_words(last_post.created_at) %>前
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
作成: <%= l board.created_at, format: :short %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右カラム: 外交関係マップ -->
|
||||
<div class="space-y-6">
|
||||
<div class="bg-white shadow rounded-lg border border-gray-200">
|
||||
<div class="px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">外交関係マップ</h3>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
交渉チャンネルを持つ国同士の繋がり(相互のみ表示)
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<!-- 簡易的なマトリックス表示 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-2 py-1"></th>
|
||||
<% @diplomacy_matrix.keys.sort.each do |power| %>
|
||||
<th class="px-2 py-1 font-bold text-gray-500 rotate-45 origin-bottom-left transform translate-x-2"><%= power[0..2] %></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<% @diplomacy_matrix.keys.sort.each do |row_power| %>
|
||||
<tr>
|
||||
<th class="px-2 py-1 font-bold text-gray-500 text-left"><%= row_power[0..2] %></th>
|
||||
<% @diplomacy_matrix.keys.sort.each do |col_power| %>
|
||||
<td class="px-2 py-1 text-center">
|
||||
<% if row_power == col_power %>
|
||||
<span class="text-gray-200">-</span>
|
||||
<% elsif @diplomacy_matrix[row_power]&.include?(col_power) %>
|
||||
<span class="text-green-600 font-bold">●</span>
|
||||
<% else %>
|
||||
<span class="text-gray-200">・</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-xs text-gray-500">
|
||||
<p>※ あなたが参加している掲示板、または公開された掲示板の関係のみ表示されます。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ヒント -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-circle-info text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">交渉のヒント</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<ul role="list" class="list-disc pl-5 space-y-1">
|
||||
<li>共通掲示板は全員が見ています。外交方針の発表に使いましょう。</li>
|
||||
<li>特定の国と密約を結ぶには「新しい掲示板を作成」から招待してください。</li>
|
||||
<li>交渉用掲示板の内容は、メンバー以外には秘密です(「公開」設定にしない限り)。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
72
app/views/boards/new.html.erb
Normal file
72
app/views/boards/new.html.erb
Normal file
@@ -0,0 +1,72 @@
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<%= link_to game_boards_path(@game), class: "text-gray-500 hover:text-gray-700" do %>
|
||||
<i class="fa-solid fa-arrow-left mr-1"></i> 掲示板一覧に戻る
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg border border-gray-200">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">新しい交渉用掲示板を作成</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
交渉したい相手(国)を選択してください。選択した相手とあなただけの秘密の掲示板が作成されます。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= form_with model: [@game, @board], local: true, class: "space-y-6" do |f| %>
|
||||
<% if @board.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4 mb-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-circle-xmark text-red-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">入力内容にエラーがあります</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul role="list" class="list-disc pl-5 space-y-1">
|
||||
<% @board.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<label class="text-base font-medium text-gray-900">招待するメンバー</label>
|
||||
<p class="text-sm text-gray-500 mb-3">少なくとも1人以上選択してください。</p>
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<% if @participants.empty? %>
|
||||
<p class="text-sm text-gray-500 italic">招待可能な他のプレイヤーがいません。</p>
|
||||
<% else %>
|
||||
<% @participants.each do |participant| %>
|
||||
<div class="relative flex items-start py-2 border rounded p-3 hover:bg-gray-50 cursor-pointer">
|
||||
<div class="min-w-0 flex-1 text-sm">
|
||||
<label for="participant_<%= participant.id %>" class="font-medium text-gray-700 select-none cursor-pointer flex items-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold border border-gray-200 mr-2 <%= power_color_class(participant.power) %>">
|
||||
<%= participant.power %>
|
||||
</span>
|
||||
<%= participant.user.username %>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ml-3 flex items-center h-5">
|
||||
<input id="participant_<%= participant.id %>" name="invited_participant_ids[]" value="<%= participant.id %>" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5 border-t border-gray-200 flex justify-end">
|
||||
<%= link_to "キャンセル", game_boards_path(@game), class: "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mr-3" %>
|
||||
<%= f.submit "掲示板を作成", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
221
app/views/boards/show.html.erb
Normal file
221
app/views/boards/show.html.erb
Normal file
@@ -0,0 +1,221 @@
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 h-screen-minus-header flex flex-col">
|
||||
<%= render "games/header", game: @game, display_turn: @game.latest_turn, current_participant: @current_participant, hide_controls: true %>
|
||||
|
||||
<!-- ヘッダー -->
|
||||
<div class="md:flex md:items-center md:justify-between mb-4 border-b pb-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<%= link_to game_boards_path(@game), class: "mr-4 text-gray-500 hover:text-gray-700" do %>
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
<% end %>
|
||||
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
||||
<% if @board.global? %>
|
||||
<i class="fa-solid fa-earth-americas text-blue-500 mr-2"></i> Global Board
|
||||
<% else %>
|
||||
<i class="fa-solid fa-handshake text-green-500 mr-2"></i> Negotiation Board
|
||||
<% end %>
|
||||
</h2>
|
||||
|
||||
<% if @board.is_public? %>
|
||||
<span class="ml-3 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<i class="fa-solid fa-bullhorn mr-1"></i> 公開中
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if @board.history_mode? %>
|
||||
<span class="ml-3 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
<i class="fa-solid fa-scroll mr-1"></i> 履歴モード
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex flex-col sm:flex-row sm:flex-wrap sm:mt-0 sm:space-x-6">
|
||||
<div class="mt-2 flex items-center text-sm text-gray-500">
|
||||
<i class="fa-solid fa-users flex-shrink-0 mr-1.5 text-gray-400"></i>
|
||||
メンバー:
|
||||
<% @active_members.each do |m| %>
|
||||
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border <%= power_color_class(m.power) %>">
|
||||
<%= m.power %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex md:mt-0 md:ml-4 space-x-2">
|
||||
<!-- 公開設定切り替え (交渉用かつ参加中のみ) -->
|
||||
<% if @board.negotiation? && @board.member?(@current_participant) && !@board.history_mode? %>
|
||||
<%= button_to toggle_public_game_board_path(@game, @board), method: :patch, class: "inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %>
|
||||
<% if @board.is_public? %>
|
||||
<i class="fa-solid fa-lock mr-1"></i> 非公開にする
|
||||
<% else %>
|
||||
<i class="fa-solid fa-bullhorn mr-1"></i> 公開する
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- 退出ボタン (交渉用かつ参加中のみ) -->
|
||||
<% if @board.negotiation? && @board.member?(@current_participant) && !@board.history_mode? %>
|
||||
<%= button_to leave_game_board_board_memberships_path(@game, @board), method: :delete, data: { turbo_confirm: "本当に退出しますか?退出後はこの掲示板に投稿できなくなります。" }, class: "inline-flex items-center px-3 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" do %>
|
||||
<i class="fa-solid fa-right-from-bracket mr-1"></i> 退出
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- 左: チャットエリア -->
|
||||
<!-- 左: チャットエリア -->
|
||||
<div class="flex-1 flex flex-col bg-white border border-gray-200 rounded-lg shadow-sm mr-4 overflow-hidden">
|
||||
|
||||
<!-- 投稿フォーム (上部に移動) -->
|
||||
<% if @board.member?(@current_participant) && !@board.history_mode? %>
|
||||
<div class="p-4 bg-gray-50 border-b border-gray-200">
|
||||
<%= form_with model: [@game, @board, @new_post], local: true, class: "flex items-end space-x-2" do |f| %>
|
||||
<div class="flex-1">
|
||||
<%= f.text_area :body, rows: 2, placeholder: "メッセージを入力...", class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md", required: true %>
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 h-10">
|
||||
<i class="fa-solid fa-paper-plane"></i>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif @board.history_mode? %>
|
||||
<div class="p-4 bg-gray-100 border-b border-gray-200 text-center text-gray-500 text-sm">
|
||||
履歴モードのため投稿できません
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="p-4 bg-gray-100 border-b border-gray-200 text-center text-gray-500 text-sm">
|
||||
この掲示板には参加していません
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- メッセージリスト -->
|
||||
<div id="board-posts"
|
||||
class="flex-1 p-4 overflow-y-auto space-y-4"
|
||||
data-controller="board-channel"
|
||||
data-board-channel-board-id-value="<%= @board.id %>"
|
||||
data-board-channel-current-participant-id-value="<%= @current_participant&.id %>">
|
||||
<% if @posts.empty? %>
|
||||
<div class="text-center text-gray-400 py-10" id="no-posts-message">
|
||||
<p>まだ投稿がありません。最初のメッセージを投稿しましょう。</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<% @posts.each do |post| %>
|
||||
<%= render partial: "post", locals: { post: post, current_participant: @current_participant } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 右: サイドパネル(メンバー管理・提案) -->
|
||||
<div class="w-80 flex flex-col space-y-4 overflow-y-auto">
|
||||
|
||||
<!-- 提案 (Proposals) -->
|
||||
<% if @board.negotiation? %>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200 font-medium text-sm text-gray-700">
|
||||
条約・提案
|
||||
</div>
|
||||
<div class="p-3 space-y-3">
|
||||
<% @board.board_proposals.order(created_at: :desc).each do |proposal| %>
|
||||
<div class="border rounded p-2 text-sm <%= proposal.status == 'accepted' ? 'bg-green-50 border-green-200' : (proposal.status == 'rejected' ? 'bg-red-50 border-red-200' : 'bg-white border-gray-200') %>">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<div>
|
||||
<span class="font-bold text-xs mr-1"><%= proposal.proposer.power %></span>
|
||||
<% if proposal.phase %>
|
||||
<span class="text-xs bg-gray-100 text-gray-500 px-1 rounded border border-gray-200"><%= proposal.phase %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400"><%= l proposal.created_at, format: :short %></span>
|
||||
</div>
|
||||
<p class="mb-2"><%= proposal.body %></p>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs font-medium px-2 py-0.5 rounded
|
||||
<%= case proposal.status
|
||||
when 'pending' then 'bg-yellow-100 text-yellow-800'
|
||||
when 'accepted' then 'bg-green-100 text-green-800'
|
||||
when 'rejected' then 'bg-red-100 text-red-800'
|
||||
end %>">
|
||||
<%= proposal.status.upcase %>
|
||||
</span>
|
||||
|
||||
<% if proposal.pending? && @board.member?(@current_participant) && !@board.history_mode? %>
|
||||
<div class="flex space-x-1">
|
||||
<%= button_to "承認", game_board_board_proposal_path(@game, @board, proposal, board_proposal: { status: 'accepted' }), method: :patch, class: "text-xs bg-green-100 hover:bg-green-200 text-green-800 px-2 py-1 rounded" %>
|
||||
<%= button_to "拒否", game_board_board_proposal_path(@game, @board, proposal, board_proposal: { status: 'rejected' }), method: :patch, class: "text-xs bg-red-100 hover:bg-red-200 text-red-800 px-2 py-1 rounded" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @board.member?(@current_participant) && !@board.history_mode? %>
|
||||
<div class="mt-2 pt-2 border-t text-center">
|
||||
<button type="button" onclick="document.getElementById('new-proposal-form').classList.toggle('hidden')" class="text-xs text-indigo-600 hover:text-indigo-800">
|
||||
+ 新しい提案を作成
|
||||
</button>
|
||||
<div id="new-proposal-form" class="hidden mt-2">
|
||||
<%= form_with model: [@game, @board, @new_proposal], local: true do |f| %>
|
||||
<%= f.text_area :body, rows: 2, class: "w-full text-xs border-gray-300 rounded mb-2", placeholder: "提案内容 (例: 1901秋冬は不可侵)" %>
|
||||
<%= f.submit "提案する", class: "w-full bg-indigo-600 text-white text-xs py-1 rounded hover:bg-indigo-700" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- メンバー招待 -->
|
||||
<% if @board.negotiation? && @board.member?(@current_participant) && !@board.history_mode? %>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200 font-medium text-sm text-gray-700">
|
||||
メンバー追加
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<% if @candidates.present? %>
|
||||
<%= form_with url: game_board_board_memberships_path(@game, @board), local: true do |f| %>
|
||||
<div class="flex space-x-2">
|
||||
<%= f.collection_select :participant_id, @candidates, :id, :power, { prompt: "国を選択" }, { class: "block w-full text-xs border-gray-300 rounded" } %>
|
||||
<%= f.submit "招待", class: "bg-indigo-600 text-white text-xs px-3 py-1 rounded hover:bg-indigo-700" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="text-xs text-gray-500">招待可能なプレイヤーがいません</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- メンバー一覧 -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 bg-gray-50 border-b border-gray-200 font-medium text-sm text-gray-700">
|
||||
参加メンバー
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<% @board.board_memberships.includes(:participant).each do |membership| %>
|
||||
<li class="px-3 py-2 flex items-center justify-between text-sm">
|
||||
<div class="flex items-center">
|
||||
<span class="<%= 'line-through text-gray-400' if membership.left_at %>">
|
||||
<%= membership.participant.power %>
|
||||
</span>
|
||||
<% if membership.participant.user == current_user %>
|
||||
<span class="ml-1 text-xs text-indigo-500">(YOU)</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if membership.left_at %>
|
||||
<span class="text-xs text-red-500">退出済</span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
133
app/views/games/_header.html.erb
Normal file
133
app/views/games/_header.html.erb
Normal file
@@ -0,0 +1,133 @@
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900"><%= h(game.title) %></h1>
|
||||
<% if game.memo.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= h(game.memo) %></p>
|
||||
<% end %>
|
||||
<div class="mt-2 flex items-center space-x-4 text-sm text-gray-600">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
<%= game.solo_mode? ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800' %>">
|
||||
<%= game.solo_mode? ? 'ソロモード' : 'マルチプレイヤーモード' %>
|
||||
</span>
|
||||
<% if game.administrator %>
|
||||
<span>管理者: <%= h(game.administrator.username) %></span>
|
||||
<% end %>
|
||||
<span>状態: <%= game.status %></span>
|
||||
</div>
|
||||
<% if game.year_limit.present? || game.victory_sc_count != 18 || game.scoring_system != "none" || game.auto_turn? %>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="font-medium text-gray-500">ハウスルール:</span>
|
||||
<% if game.auto_turn? %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-blue-100 text-blue-800">
|
||||
<i class="fa-solid fa-clock mr-1"></i> <%= game.schedule_display %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% if game.year_limit.present? %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
|
||||
<i class="fa-solid fa-hourglass-half mr-1"></i> <%= game.year_limit %>年制限
|
||||
</span>
|
||||
<% end %>
|
||||
<% if game.victory_sc_count != 18 %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-green-100 text-green-800">
|
||||
<i class="fa-solid fa-crown mr-1"></i> 目標SC: <%= game.victory_sc_count %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% if game.scoring_system != "none" %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800">
|
||||
<i class="fa-solid fa-chart-bar mr-1"></i> <%= game.scoring_system_name %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if game.auto_turn? && game.next_deadline_at.present? && game.status == "in_progress" %>
|
||||
<div class="mt-2 px-3 py-1.5 rounded-md bg-blue-50 border border-blue-200 text-sm text-blue-800 flex items-center gap-2">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<span>次のターン締切: <strong><%= game.next_deadline_at.in_time_zone("Asia/Tokyo").strftime("%Y-%m-%d %H:%M") %></strong></span>
|
||||
<span class="text-blue-600" id="deadline-countdown"></span>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const deadline = new Date("<%= game.next_deadline_at.iso8601 %>");
|
||||
const countdownEl = document.getElementById('deadline-countdown');
|
||||
function updateCountdown() {
|
||||
const now = new Date();
|
||||
const diff = deadline - now;
|
||||
if (diff <= 0) {
|
||||
countdownEl.textContent = '(締切済み — まもなく処理されます)';
|
||||
return;
|
||||
}
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
countdownEl.textContent = `(残り ${hours}時間 ${minutes}分)`;
|
||||
}
|
||||
if (countdownEl) {
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 60000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<% unless local_assigns[:hide_controls] %>
|
||||
<div class="flex space-x-3">
|
||||
<% if current_user&.admin? || game.administrator == current_user %>
|
||||
<%= link_to "設定", edit_game_path(game), class: "inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
|
||||
<%= button_to "削除", game, method: :delete, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500", data: { turbo_confirm: "本当に削除しますか?" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% # ゲーム画面(games/show)と統一されたデザインでターン情報を表示 %>
|
||||
<%
|
||||
target_turn = defined?(display_turn) && display_turn ? display_turn : game.latest_turn
|
||||
%>
|
||||
<% if target_turn && target_turn.phase.present? %>
|
||||
<%
|
||||
# フェーズ名パース (例: S1901M)
|
||||
phase = target_turn.phase
|
||||
year = phase[1..4]
|
||||
season_code = phase[0]
|
||||
type_code = phase[-1]
|
||||
|
||||
season = case season_code
|
||||
when 'S' then '春'
|
||||
when 'F' then '秋'
|
||||
when 'W' then '冬'
|
||||
else ''
|
||||
end
|
||||
|
||||
type = case type_code
|
||||
when 'M' then '移動'
|
||||
when 'R' then '撤退'
|
||||
when 'A' then '調整'
|
||||
else ''
|
||||
end
|
||||
|
||||
display_date = "#{year}年 #{season} (#{type})"
|
||||
%>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg border border-gray-200 mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900"><%= display_date %></h2>
|
||||
<div class="mt-1 flex items-center space-x-4 text-sm text-gray-600">
|
||||
<span>ターン: <%= target_turn.number %></span>
|
||||
<span class="text-gray-400">|</span>
|
||||
<span>フェーズ: <%= phase %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<% if defined?(current_participant) && current_participant&.power %>
|
||||
<div class="flex items-center bg-white px-4 py-2 rounded border border-gray-300 shadow-sm">
|
||||
<span class="mr-2 text-sm text-gray-500">あなたの担当国:</span>
|
||||
<span class="text-lg font-bold text-indigo-700"><%= current_participant.power %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -414,6 +414,45 @@
|
||||
|
||||
<!-- 右カラム: 情報パネル (1/3) -->
|
||||
<div class="space-y-6">
|
||||
<!-- 掲示板リンク -->
|
||||
<div class="bg-white shadow rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-indigo-100 rounded-md p-3">
|
||||
<i class="fa-solid fa-comments text-indigo-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">外交・交渉掲示板</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
秘密交渉や全体アナウンスはこちら
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= link_to game_boards_path(@game), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %>
|
||||
掲示板へ
|
||||
<%
|
||||
# 未読件数取得(参加中の全掲示板の合計)
|
||||
current_participant = @game.participants.find_by(user: current_user)
|
||||
if current_participant
|
||||
total_unread = 0
|
||||
current_participant.boards.each do |board|
|
||||
total_unread += board.unread_count_for(current_participant)
|
||||
end
|
||||
if total_unread > 0
|
||||
%>
|
||||
<span class="ml-2 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 bg-red-600 rounded-full">
|
||||
<%= total_unread %>
|
||||
</span>
|
||||
<%
|
||||
end
|
||||
end
|
||||
%>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 国ステータス表 -->
|
||||
<div class="bg-white shadow rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<%= action_cable_meta_tag %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user