summaryrefslogtreecommitdiff
path: root/views
diff options
context:
space:
mode:
Diffstat (limited to 'views')
-rw-r--r--views/TEMPLATE13
-rw-r--r--views/about.ejs75
-rw-r--r--views/about.pug41
-rw-r--r--views/change_about.ejs11
-rw-r--r--views/change_about.pug27
-rw-r--r--views/change_mail.ejs12
-rw-r--r--views/change_mail.pug21
-rw-r--r--views/change_name.ejs12
-rw-r--r--views/change_name.pug21
-rw-r--r--views/change_password.ejs15
-rw-r--r--views/change_password.pug26
-rw-r--r--views/chat.ejs79
-rw-r--r--views/chat.pug93
-rw-r--r--views/create.ejs32
-rw-r--r--views/create.pug40
-rw-r--r--views/error.ejs1
-rw-r--r--views/forgot_password.ejs13
-rw-r--r--views/forgot_password.pug23
-rw-r--r--views/forum_edit.ejs11
-rw-r--r--views/forum_edit.pug25
-rw-r--r--views/forum_post.ejs20
-rw-r--r--views/forum_post.pug44
-rw-r--r--views/forum_reply.ejs23
-rw-r--r--views/forum_reply.pug39
-rw-r--r--views/forum_thread.ejs29
-rw-r--r--views/forum_thread.pug37
-rw-r--r--views/forum_view.ejs37
-rw-r--r--views/forum_view.pug41
-rw-r--r--views/games.ejs51
-rw-r--r--views/games.pug18
-rw-r--r--views/head.pug67
-rw-r--r--views/header.ejs36
-rw-r--r--views/header.pug15
-rw-r--r--views/index.ejs29
-rw-r--r--views/index.pug49
-rw-r--r--views/info.ejs80
-rw-r--r--views/info.pug32
-rw-r--r--views/join.ejs52
-rw-r--r--views/join.pug58
-rw-r--r--views/login.ejs17
-rw-r--r--views/login.pug29
-rw-r--r--views/message_inbox.ejs21
-rw-r--r--views/message_inbox.pug31
-rw-r--r--views/message_outbox.ejs28
-rw-r--r--views/message_outbox.pug36
-rw-r--r--views/message_read.ejs25
-rw-r--r--views/message_read.pug38
-rw-r--r--views/message_send.ejs33
-rw-r--r--views/message_send.pug68
-rw-r--r--views/profile.ejs102
-rw-r--r--views/profile.pug49
-rw-r--r--views/reset_password.ejs14
-rw-r--r--views/reset_password.pug28
-rw-r--r--views/signup.ejs18
-rw-r--r--views/signup.pug31
-rw-r--r--views/stats.ejs39
-rw-r--r--views/stats.pug45
-rw-r--r--views/user.ejs15
-rw-r--r--views/user.pug23
-rw-r--r--views/user_list.pug26
-rw-r--r--views/users.ejs14
61 files changed, 1134 insertions, 944 deletions
diff --git a/views/TEMPLATE b/views/TEMPLATE
new file mode 100644
index 0000000..06f5a94
--- /dev/null
+++ b/views/TEMPLATE
@@ -0,0 +1,13 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title TITLE
+ body
+ include header
+ article
+ h1 TITLE
+ if flash
+ p.error= flash
+
diff --git a/views/about.ejs b/views/about.ejs
deleted file mode 100644
index 4418d52..0000000
--- a/views/about.ejs
+++ /dev/null
@@ -1,75 +0,0 @@
-<%- include('header', { title: "Rally the Troops!" }) -%>
-<style>
-li img{height:1.0em;vertical-align:middle}
-</style>
-
-<p>
-Rally the Troops! is created and maintained by Tor Andersson.
-It is an open source project.
-
-<p>
-Please submit problem reports and make suggestions for improvements on
-<a href="https://github.com/ccxvii/rally-the-troops/issues">GitHub</a>.
-
-<h2>
-Tips &amp; Tricks
-</h2>
-
-<ul>
-
-<li>
-Open a separate browser tab or window for each side when playing solo.
-
-<li>
-Use the middle mouse button to drag and scroll around the map.
-
-<li>
-Hold down the Shift key when mousing over a block or counter in order to magnify it.
-
-<li>
-The <i>Enter</i> and <i>Escape</i> keys open and close the chat box.
-
-<li>
-To invite your friends to a private game, send them the address of the join page.
-
-<li>
-Chat messages can only be seen by players who are part of a game.
-They are hidden from observers.
-
-<li>
-The <img src="/images/cog.svg"> menu has links to rules, player aids and other reference material.
-In some games you can also choose between alternative graphics and layout options.
-
-<li>
-The <img src="/images/earth-africa-europe.svg"> button hides all counters and markers,
-if you need to check something on the map that is obscured.
-
-<li>
-The <img src="/images/magnifying-glass.svg"> button shrinks the map to fit the screen width.
-
-<li>
-The <img src="/images/scroll-quill.svg"> button hides the game log and player status displays, so you can
-see more of the map.
-
-<li>
-The <img src="/images/chat-bubble.svg"> button lights up if you have unread chat messages.
-
-<li>
-The <img src="/images/cycle.svg"> button appears when the game is over, and can be used to quickly start a rematch.
-
-</ul>
-
-<h2>
-Licensing
-</h2>
-
-<p>
-All games are used with consent from their respective rights holders.
-
-<p>
-Icons are sourced from <a href="https://game-icons.net">game-icons.net</a>
-by Delapouite, Lorc, and others under the
-<a href="https://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a> license.
-<p>
-Other images and graphics are sourced from
-<a href="https://commons.wikimedia.org/wiki/Main_Page">Wikimedia Commons</a>.
diff --git a/views/about.pug b/views/about.pug
new file mode 100644
index 0000000..382ad59
--- /dev/null
+++ b/views/about.pug
@@ -0,0 +1,41 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Rally the Troops!
+ style.
+ li img { height: 1.0em; vertical-align: middle; }
+ body
+ include header
+ article
+ h1 Rally the Troops!
+
+ p Rally the Troops! is created and maintained by Tor Andersson. It is an open source project.
+
+ p Please submit problem reports and make suggestions for improvements on #[a(href="https://github.com/ccxvii/rally-the-troops/issues") GitHub].
+
+ h2 Tips & Tricks
+
+ ul
+ li Open a separate browser tab or window for each side when playing solo.
+ li Use the middle mouse button to drag and scroll around the map.
+ li Hold down the Shift key when mousing over a block or counter in order to magnify it.
+ li The #[i Enter] and #[i Escape] keys open and close the chat box.
+ li To invite your friends to a private game, send them the address of the join page.
+ li Chat messages can only be seen by players who are part of a game. They are hidden from observers.
+ li The #[img(src="/images/cog.svg")] menu has links to rules, player aids and other reference material. In some games you can also choose between alternative graphics and layout options.
+ li The #[img(src="/images/earth-africa-europe.svg")] button hides all counters and markers, if you need to check something on the map that is obscured.
+ li The #[img(src="/images/magnifying-glass.svg")] button shrinks the map to fit the screen width.
+ li The #[img(src="/images/scroll-quill.svg")] button hides the game log and player status displays, so you can see more of the map.
+ li The #[img(src="/images/chat-bubble.svg")] button lights up if you have unread chat messages.
+ li The #[img(src="/images/cycle.svg")] button appears when the game is over, and can be used to quickly start a rematch.
+
+ h2 Licensing
+
+ p All games are used with consent from their respective rights holders.
+
+ p Icons are sourced from #[a(href="https://game-icons.net") game-icons.net] by Delapouite, Lorc, and others under the #[a(href="https://creativecommons.org/licenses/by/3.0/") CC BY 3.0] license.
+
+ p Other images and graphics are sourced from #[a( href="https://commons.wikimedia.org/wiki/Main_Page") Wikimedia Commons].
+
diff --git a/views/change_about.ejs b/views/change_about.ejs
deleted file mode 100644
index cbea0c5..0000000
--- a/views/change_about.ejs
+++ /dev/null
@@ -1,11 +0,0 @@
-<%- include('header', { title: "Change profile text" }) -%>
-<style>
-textarea { width: 100%; max-width: 45em; }
-</style>
-<form action="/change_about" method="post">
-<p>
-<textarea name="about" rows="20" cols="80" maxlength="32000" autofocus>
-<%= about %></textarea>
-<p>
-<button type="submit">Submit</button>
-</form>
diff --git a/views/change_about.pug b/views/change_about.pug
new file mode 100644
index 0000000..3232c26
--- /dev/null
+++ b/views/change_about.pug
@@ -0,0 +1,27 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Change profile text
+ style.
+ input, textarea { width: min(45rem,100%) }
+ body
+ include header
+ article
+ h1 Change profile text
+ form(method="post" action="/change_about")
+ p Name: #{user.name}
+ p Mail: #{user.mail}
+ p
+ textarea(
+ name="about"
+ rows=20
+ cols=80
+ maxlength=32000
+ autofocus
+ )
+ |
+ | #{about}
+ p
+ button(type="submit") Submit
diff --git a/views/change_mail.ejs b/views/change_mail.ejs
deleted file mode 100644
index 6c3ed62..0000000
--- a/views/change_mail.ejs
+++ /dev/null
@@ -1,12 +0,0 @@
-<%- include('header', { title: "Change mail address" }) %>
-<form action="/change_mail" method="post">
-<p>
-Name: <%= user.name %>
-<p>
-Mail: <%= user.mail %>
-<p>
-<label for="newmail">New mail address: </label><br>
-<input type="newmail" id="newmail" name="newmail" required>
-<p>
-<button type="submit">Change mail</button>
-</form>
diff --git a/views/change_mail.pug b/views/change_mail.pug
new file mode 100644
index 0000000..77ddfc2
--- /dev/null
+++ b/views/change_mail.pug
@@ -0,0 +1,21 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Change mail address
+ body
+ include header
+ article
+ h1 Change mail address
+ if flash
+ p.error= flash
+ form(method="post" action="/change_mail")
+ p Name: #{user.name}
+ p Mail: #{user.mail}
+ p
+ label New mail address:
+ br
+ input(type="text" name="newmail" required)
+ p
+ button(type="submit") Change mail
diff --git a/views/change_name.ejs b/views/change_name.ejs
deleted file mode 100644
index eac508f..0000000
--- a/views/change_name.ejs
+++ /dev/null
@@ -1,12 +0,0 @@
-<%- include('header', { title: "Change name" }) %>
-<form action="/change_name" method="post">
-<p>
-Name: <%= user.name %>
-<p>
-Mail: <%= user.mail %>
-<p>
-<label for="newname">New name: </label><br>
-<input type="newname" id="newname" name="newname" required>
-<p>
-<button type="submit">Change name</button>
-</form>
diff --git a/views/change_name.pug b/views/change_name.pug
new file mode 100644
index 0000000..2cded48
--- /dev/null
+++ b/views/change_name.pug
@@ -0,0 +1,21 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Change name
+ body
+ include header
+ article
+ h1 Change name
+ if flash
+ p.error= flash
+ form(method="post" action="/change_name")
+ p Name: #{user.name}
+ p Mail: #{user.mail}
+ p
+ label New name:
+ br
+ input(type="text" name="newname" required)
+ p
+ button(type="submit") Change name
diff --git a/views/change_password.ejs b/views/change_password.ejs
deleted file mode 100644
index ab15a4a..0000000
--- a/views/change_password.ejs
+++ /dev/null
@@ -1,15 +0,0 @@
-<%- include('header', { title: "Change password" }) %>
-<form action="/change_password" method="post">
-<p>
-Name: <%= user.name %>
-<p>
-Mail: <%= user.mail %>
-<p>
-<label for="password">Old Password: </label><br>
-<input type="password" id="password" name="password" required>
-<p>
-<label for="newpass">New Password: </label><br>
-<input type="password" id="newpass" name="newpass" required>
-<p>
-<button type="submit">Change password</button>
-</form>
diff --git a/views/change_password.pug b/views/change_password.pug
new file mode 100644
index 0000000..feaa46c
--- /dev/null
+++ b/views/change_password.pug
@@ -0,0 +1,26 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Change password
+ body
+ include header
+ article
+ h1 Change password
+ if flash
+ p.error= flash
+
+ form(method="post" action="/change_password")
+ p Name: #{user.name}
+ p Mail: #{user.mail}
+ p
+ label Old Password:
+ br
+ input(type="password" name="password" required)
+ p
+ label New Password:
+ br
+ input(type="password" name="newpass" required)
+ p
+ button(type="submit") Change password
diff --git a/views/chat.ejs b/views/chat.ejs
deleted file mode 100644
index d217857..0000000
--- a/views/chat.ejs
+++ /dev/null
@@ -1,79 +0,0 @@
-<%- include('header', { title: "Chat Log" }) -%>
-<style>
-table{width:min(60rem,100%);}
-td:nth-child(1),td:nth-child(2){white-space:nowrap;width:0}
-td:nth-child(3){white-space:nowrap;width:8rem}
-#foot{display:inline-block;padding-top:8px;}
-table{border:1px solid black}
-td{border:none}
-td a{text-decoration:none;color:black;}
-tr{background-color:white;}
-tr.me{background-color:aliceblue;}
-</style>
-<% if (page_size > 0) { %>
-<p>
-<button onclick="oldest()">Oldest</button>
-<button onclick="back()">&#x2190;</button>
-<button onclick="next()">&#x2192;</button>
-<button onclick="newest()">Newest</button>
-<span id="foot"></span>
-<table><thead><tr><th>Game<th>Time<th>Who<th>Message</thead><tbody></tbody></table>
-<p><a href="/chat/all">All messages</a>
-<% } else { %>
-<table><thead><tr><th>Game<th>Time<th>Who<th>Message</thead><tbody></tbody></table>
-<% } %>
-<script>
-let chat_data = <%- JSON.stringify(chat).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;") %>;
-let page_size = <%- page_size %>;
-let me = <%- JSON.stringify(user.name) %>;
-let table = document.querySelector("tbody");
-let foot = document.querySelector("#foot");
-let chat_lines = [];
-function add(tr,text,link) {
- let td = document.createElement("td");
- if (link) {
- let a = document.createElement("a");
- a.href = link;
- a.textContent = text;
- td.appendChild(a);
- } else {
- td.textContent = text;
- }
- tr.appendChild(td);
-}
-function format_date(date) {
- let t = date.toISOString();
- return t.substring(0,10) + " " + t.substring(11,19);
-}
-for (let [game_id,time,user,message] of chat_data) {
- let tr = document.createElement("tr");
- if (user === me) tr.className = 'me';
- add(tr,game_id,'/join/'+game_id);
- add(tr,format_date(new Date(time+'Z')));
- add(tr,user,'/user/'+user);
- add(tr,message);
- chat_lines.push(tr);
-}
-function next() { if (page > 0) --page; update(); }
-function back() { if (page < page_count-1) ++page; update(); }
-function newest() { page=0; update(); }
-function oldest() { page=page_count-1; update(); }
-let chat_size = chat_lines.length;
-let page_count = Math.ceil(chat_size / page_size);
-let page = 0;
-function update() {
- table.innerHTML = '';
- for (let i = 0; i < page_size; ++i) {
- let k = page * page_size + ( page_size - i - 1);
- if (k >= 0 && k < chat_size)
- table.appendChild(chat_lines[k]);
- foot.textContent = `${page_count-page} / ${page_count}`;
- }
-}
-if (page_size > 0) {
- newest();
-} else {
- for (let i = chat_size-1; i >= 0; --i)
- table.appendChild(chat_lines[i]);
-}
-</script>
diff --git a/views/chat.pug b/views/chat.pug
new file mode 100644
index 0000000..dfe6f25
--- /dev/null
+++ b/views/chat.pug
@@ -0,0 +1,93 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Chat Log
+ style.
+ table { width: min(60rem,100%); }
+ td:nth-child(1), td:nth-child(2) { white-space: nowrap; width: 0; }
+ td:nth-child(3) { white-space: nowrap; width: 8rem; }
+ #foot { display: inline-block; padding-top: 8px; }
+ table { border: 1px solid black; }
+ td { border: none; }
+ td a { text-decoration: none; color: black; }
+ tr { background-color: white; }
+ tr.me { background-color: aliceblue; }
+ body
+ include header
+ article
+ h1 Chat Log
+ if page_size > 0
+ p
+ button(onclick="oldest()") Oldest
+ button(onclick="back()") &#x2190;
+ button(onclick="next()") &#x2192;
+ button(onclick="newest()") Newest
+ span#foot
+ p
+ table
+ thead
+ tr
+ th Game
+ th Time
+ th Who
+ th Message
+ tbody
+ if page_size > 0
+ p: a(href="/chat/all") All messages
+
+ script.
+ let chat_data = !{ JSON.stringify(chat).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;") };
+ let page_size = !{ page_size };
+ let me = !{ JSON.stringify(user.name) };
+ let table = document.querySelector("tbody");
+ let foot = document.querySelector("#foot");
+ let chat_lines = [];
+ function add(tr,text,link) {
+ let td = document.createElement("td");
+ if (link) {
+ let a = document.createElement("a");
+ a.href = link;
+ a.textContent = text;
+ td.appendChild(a);
+ } else {
+ td.textContent = text;
+ }
+ tr.appendChild(td);
+ }
+ function format_date(date) {
+ let t = date.toISOString();
+ return t.substring(0,10) + " " + t.substring(11,19);
+ }
+ for (let [game_id,time,user,message] of chat_data) {
+ let tr = document.createElement("tr");
+ if (user === me) tr.className = "me";
+ add(tr,game_id,"/join/"+game_id);
+ add(tr,format_date(new Date(time+"Z")));
+ add(tr,user,"/user/"+user);
+ add(tr,message);
+ chat_lines.push(tr);
+ }
+ function next() { if (page > 0) --page; update(); }
+ function back() { if (page < page_count-1) ++page; update(); }
+ function newest() { page=0; update(); }
+ function oldest() { page=page_count-1; update(); }
+ let chat_size = chat_lines.length;
+ let page_count = Math.ceil(chat_size / page_size);
+ let page = 0;
+ function update() {
+ table.innerHTML = "";
+ for (let i = 0; i < page_size; ++i) {
+ let k = page * page_size + ( page_size - i - 1);
+ if (k >= 0 && k < chat_size)
+ table.appendChild(chat_lines[k]);
+ foot.textContent = `${page_count-page} / ${page_count}`;
+ }
+ }
+ if (page_size > 0) {
+ newest();
+ } else {
+ for (let i = chat_size-1; i >= 0; --i)
+ table.appendChild(chat_lines[i]);
+ }
diff --git a/views/create.ejs b/views/create.ejs
deleted file mode 100644
index 36b86e2..0000000
--- a/views/create.ejs
+++ /dev/null
@@ -1,32 +0,0 @@
-<%- include('header', { title: title.title_name }) %>
-<style>form{display:block;margin-left:170px;}</style>
-<a href="/info/<%= title.title_id %>"><img class="logo" src="/<%= title.title_id %>/cover.jpg"></a>
-<form action="/create/<%= title.title_id %>" method="post">
-<p>
-<% if (scenarios.length > 1) { %>
-Scenario:<br>
-<select name="scenario">
-<% scenarios.forEach((scenario) => { %>
-<option value="<%= scenario %>"><%= scenario %></option>
-<% }); %>
-</select>
-<% } else { %>
-<input type="hidden" name="scenario" value="<%= scenarios[0] %>">
-<% } %>
-<%- include('../public/' + title.title_id + '/create.html') %>
-<p>
-Description:<br>
-<input type="text" autocomplete="off" name="description" size="50">
-<p>
-<label>
-<input type="checkbox" name="random" value="true">
-Random player roles
-</label>
-<p>
-<label>
-<input type="checkbox" name="private" value="true">
-Private
-</label>
-<p>
-<button type="submit">Create</button>
-</form>
diff --git a/views/create.pug b/views/create.pug
new file mode 100644
index 0000000..9196aa9
--- /dev/null
+++ b/views/create.pug
@@ -0,0 +1,40 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= title.title_name
+ style.
+ form { margin-left: 170px; }
+ body
+ include header
+ article
+ h1= title.title_name
+ if flash
+ p.error= flash
+
+ a(href="/info/"+title.title_id): img.logo(src="/"+title.title_id+"/cover.jpg")
+
+ form(method="post" action="/create/"+title.title_id)
+ if scenarios.length > 1
+ p Scenario:
+ br
+ select(name="scenario")
+ each scenario in scenarios
+ option(value=scenario)= scenario
+ else
+ input(type="hidden" name="scenario" value=scenarios[0])
+ | !{ create_html }
+ p Description:
+ br
+ input(type="text" autocomplete="off" name="description" size=50)
+ p
+ label
+ input(type="checkbox" name="random" value="true")
+ | Random player roles
+ p
+ label
+ input(type="checkbox" name="private" value="true")
+ | Private
+ p
+ button(type="submit") Create
diff --git a/views/error.ejs b/views/error.ejs
deleted file mode 100644
index b7d9632..0000000
--- a/views/error.ejs
+++ /dev/null
@@ -1 +0,0 @@
-<%- include('header', { title: "Error" }) %>
diff --git a/views/forgot_password.ejs b/views/forgot_password.ejs
deleted file mode 100644
index 1679496..0000000
--- a/views/forgot_password.ejs
+++ /dev/null
@@ -1,13 +0,0 @@
-<%- include('header', { title: "Forgot password" }) %>
-<% if (user) { %>
-<p>
-You're already logged in!
-<% } else { %>
-<form action="/forgot_password" method="post">
-<p>
-<label for="mail">Mail: </label><br>
-<input type="mail" id="mail" name="mail" required>
-<p>
-<button type="submit">Forgot password</button>
-</form>
-<% } %>
diff --git a/views/forgot_password.pug b/views/forgot_password.pug
new file mode 100644
index 0000000..ea53ea4
--- /dev/null
+++ b/views/forgot_password.pug
@@ -0,0 +1,23 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Forgot password
+ body
+ include header
+ article
+ h1 Forgot password
+ if flash
+ p.error= flash
+
+ if user
+ p You're already logged in!
+ else
+ form(method="post" action="/forgot_password")
+ p
+ label Mail:
+ br
+ input(type="mail" name="mail" required)
+ p
+ button(type="submit") Forgot password
diff --git a/views/forum_edit.ejs b/views/forum_edit.ejs
deleted file mode 100644
index 7ed1414..0000000
--- a/views/forum_edit.ejs
+++ /dev/null
@@ -1,11 +0,0 @@
-<%- include('header', { title: "Edit Post" }) -%>
-<style>
-input, textarea { width: 100%; max-width: 45em; }
-</style>
-<form action="/forum/edit/<%- post.post_id %>" method="post">
-<p>
-<textarea name="body" rows="20" cols="80" maxlength="32000" required autofocus>
-<%= post.body %></textarea>
-<p>
-<button type="submit">Submit</button>
-</form>
diff --git a/views/forum_edit.pug b/views/forum_edit.pug
new file mode 100644
index 0000000..ff2a5b1
--- /dev/null
+++ b/views/forum_edit.pug
@@ -0,0 +1,25 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Edit Post
+ style.
+ input, textarea { width: min(45rem,100%) }
+ body
+ include header
+ article
+ h1 Edit Post
+
+ form(method="post" action="/forum/edit/"+post.post_id)
+ textarea(
+ name="body"
+ rows=20
+ cols=80
+ maxlength=32000
+ required
+ )
+ |
+ | #{post.body}
+ p
+ button(type="submit") Submit
diff --git a/views/forum_post.ejs b/views/forum_post.ejs
deleted file mode 100644
index 98d667c..0000000
--- a/views/forum_post.ejs
+++ /dev/null
@@ -1,20 +0,0 @@
-<%- include('header', { title: "New Thread" }) -%>
-<style>
-input, textarea { width: 100%; max-width: 45em; }
-</style>
-<form action="/forum/post" method="post">
-<p>
-Subject:
-<br>
-<input type="text" name="subject" size="80" maxlength="80"
- onkeypress="if(event.keyCode===13){document.querySelector('textarea').focus();return false;}"
- autofocus
- pattern=".*\S+.*"
- required>
-<p>
-Body:
-<br>
-<textarea name="body" rows="20" cols="80" maxlength="32000" required></textarea>
-<p>
-<button type="submit">Submit</button>
-</form>
diff --git a/views/forum_post.pug b/views/forum_post.pug
new file mode 100644
index 0000000..d3c0e80
--- /dev/null
+++ b/views/forum_post.pug
@@ -0,0 +1,44 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title New Thread
+ style.
+ input, textarea { width: min(45rem,100%) }
+ script.
+ function next(event,sel) {
+ if (event.keyCode === 13) {
+ document.querySelector(sel).focus();
+ return false;
+ }
+ return true;
+ }
+ body
+ include header
+ article
+ h1 New Thread
+ form(method="post" action="/forum/post")
+ p Subject:
+ br
+ input(
+ type="text"
+ name="subject"
+ size=80
+ maxlength=80
+ required
+ pattern=".*\\S+.*"
+ autofocus
+ onkeypress="return next(event,'textarea')"
+ )
+ p Body:
+ br
+ textarea(
+ name="body"
+ rows=20
+ cols=80
+ maxlength=32000
+ required
+ )
+ p
+ button(type="submit") Submit
diff --git a/views/forum_reply.ejs b/views/forum_reply.ejs
deleted file mode 100644
index db777b6..0000000
--- a/views/forum_reply.ejs
+++ /dev/null
@@ -1,23 +0,0 @@
-<%- include('header', { title: thread.subject }) -%>
-<style>
-input, textarea { width: 100%; max-width: 45em; }
-table { max-width: 50em; }
-table .author { border-right: none; }
-table .time { border-left: none; font-weight: normal; }
-table .command { border: none; }
-</style>
-<table class="post">
-<tr>
-<th class="author"><%= post.author_name %>
-<th class="r time"><%= post.ctime %> <%= post.edited ? "(edited " + post.mtime + ")" : "" %>
-<tr>
-<td class="body" colspan="2"><%- post.body %></td>
-</table>
-<form action="/forum/reply/<%- thread.thread_id %>" method="post">
-<p>
-Reply:
-<br>
-<textarea name="body" rows="15" cols="80" maxlength="32000" required autofocus></textarea>
-<p>
-<button type="submit">Submit</button>
-</form>
diff --git a/views/forum_reply.pug b/views/forum_reply.pug
new file mode 100644
index 0000000..5b5a3bb
--- /dev/null
+++ b/views/forum_reply.pug
@@ -0,0 +1,39 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= thread.subject
+ style.
+ input, textarea { width: min(45rem,100%) }
+ table { max-width: 50em; }
+ table .author { border-right: none; }
+ table .time { border-left: none; font-weight: normal; }
+ table .command { border: none; }
+ body
+ include header
+ article
+ h1= thread.subject
+
+ table
+ tr
+ th.author: a(href="/user/"+post.author_name)= post.author_name
+ th.r.time= post.ctime
+ if post.edited
+ |
+ | (edited #{post.mtime})
+ tr
+ td.body(colspan=2)!= post.body
+
+ form(method="post" action="/forum/reply/"+thread.thread_id)
+ p Reply:
+ br
+ textarea(
+ name="body"
+ rows=15
+ cols=80
+ maxlength=32000
+ required
+ )
+ p
+ button(type="submit") Submit
diff --git a/views/forum_thread.ejs b/views/forum_thread.ejs
deleted file mode 100644
index 5c76704..0000000
--- a/views/forum_thread.ejs
+++ /dev/null
@@ -1,29 +0,0 @@
-<%- include('header', { title: thread.subject }) -%>
-<style>
-table { max-width: 50em; }
-table .author { border-right: none; }
-table .time { border-left: none; font-weight: normal; }
-table .command { border: none; }
-</style>
-<% posts.forEach((row) => { %>
-<p>
-<table class="post">
-<tr>
-<th class="author"><a href="/user/<%- row.author_name %>"><%= row.author_name %></a>
-<th class="r time"><%= row.ctime %>
-<%= row.edited ? "(edited " + row.mtime + ")" : "" %>
-<tr>
-<td class="body" colspan="2"><%- row.body %></td>
-<% if (user) { %>
-<tr>
-<td class="r command" colspan=2>
-<% if (row.author_id === user.user_id) { %>
-<a href="/forum/edit/<%- row.post_id %>">Edit</a>
-<% } %>
-<a href="/forum/reply/<%- row.post_id %>">Reply</a>
-<% } %>
-</table>
-<% }); %>
-<% if (user) { %>
-<p><a href="/forum/reply/<%- posts[0].post_id %>">Reply</a>
-<% } %>
diff --git a/views/forum_thread.pug b/views/forum_thread.pug
new file mode 100644
index 0000000..bb19b61
--- /dev/null
+++ b/views/forum_thread.pug
@@ -0,0 +1,37 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= thread.subject
+ style.
+ table { max-width: 50em; }
+ table .author { border-right: none; }
+ table .time { border-left: none; font-weight: normal; }
+ table .command { border: none; }
+ body
+ include header
+ article
+ h1= thread.subject
+
+ each row in posts
+ p
+ table
+ tr
+ th.author: a(href="/user/"+row.author_name)= row.author_name
+ th.r.time= row.ctime
+ if row.edited
+ |
+ | (edited #{row.mtime})
+ tr
+ td.body(colspan=2)!= row.body
+ if user
+ tr
+ td.r.command(colspan=2)
+ if row.author_id === user.user_id
+ | #[a(href="/forum/edit/"+row.post_id) Edit]
+ |
+ | #[a(href="/forum/reply/"+row.post_id) Reply]
+
+ if user
+ p: a(href="/forum/reply/"+posts[0].post_id) Reply
diff --git a/views/forum_view.ejs b/views/forum_view.ejs
deleted file mode 100644
index d5ebd46..0000000
--- a/views/forum_view.ejs
+++ /dev/null
@@ -1,37 +0,0 @@
-<%- include('header', { title: "Forum", refresh: 900 }) -%>
-<style>
-tfoot{background-color:gainsboro}
-</style>
-<table>
-<tr>
-<th>Subject
-<th>Author
-<th>Replies
-<th>Time
-<% threads.forEach((row) => { %>
-<tr>
-<td><a href="/forum/thread/<%- row.thread_id %>"><%= row.subject %></a>
-<td><a href="/user/<%- row.author_name %>"><%= row.author_name %></a>
-<td><%= row.replies %>
-<td><%= row.mtime %>
-<% }); %>
-<tfoot>
-<tr>
-<td colspan="4">
-<% if (current_page > 1) { %>
-<a href="/forum/page/<%- current_page-1 %>">&#x2190;</a>
-<% } %>
-<% for (let p = 1; p <= page_count && p <= 30; ++p) { %>
-<% if (p === current_page) { %>
-(<%- p %>)
-<% } else { %>
-<a href="/forum/page/<%- p %>"><%- p %></a>
-<% } %>
-<% } %>
-<% if (current_page < page_count) { %>
-<a href="/forum/page/<%- current_page+1 %>">&#x2192;</a>
-<% } %>
-</table>
-<% if (user) { %>
-<p><a href="/forum/post">New thread</a>
-<% } %>
diff --git a/views/forum_view.pug b/views/forum_view.pug
new file mode 100644
index 0000000..0b9078c
--- /dev/null
+++ b/views/forum_view.pug
@@ -0,0 +1,41 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Forum
+ meta(http-equiv="refresh" content=900)
+ style tfoot { background-color: gainsboro }
+ body
+ include header
+ article
+ h1 Forum
+
+ table
+ tr
+ th Subject
+ th Author
+ th Replies
+ th Time
+ each row in threads
+ tr
+ td: a(href="/forum/thread/"+row.thread_id)= row.subject
+ td: a(href="/forum/thread/"+row.author_name)= row.author_name
+ td= row.replies
+ td= row.mtime
+ tfoot: tr: td(colspan=4)
+ if current_page > 1
+ | #[a(href="/forum/page/"+(current_page-1)) &#x2190;]
+ |
+ - for (let p=1; p<=page_count && p<=30; ++p)
+ if p === current_page
+ | (#{p})
+ |
+ else
+ | #[a(href="/forum/page/"+p)= p]
+ |
+ if current_page < page_count
+ | #[a(href="/forum/page/"+(current_page+1)) &#x2192;]
+
+ if user
+ p: a(href="/forum/post") New thread
diff --git a/views/games.ejs b/views/games.ejs
deleted file mode 100644
index 90b2f68..0000000
--- a/views/games.ejs
+++ /dev/null
@@ -1,51 +0,0 @@
-<%- include('header', { title: "All Public Games", refresh: (user ? 300 : 0) }) -%>
-
-<h2>Open</h2>
-<table>
-<tr><th>ID<th>Title<th>Scenario<th>Players<th>Description<th>Created<th>
-<% if (open_games.length > 0) { %>
-<% open_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><a href="/info/<%= row.title_id %>"><%= row.title_name %></a>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.ctime %>
-<td class="command"><a href="/join/<%= row.game_id %>">Join</a>
-<% }); } else { %>
-<tr><td colspan="7">No open games.
-<% } %>
-</table>
-
-<h2>Active</h2>
-<table>
-<tr><th>ID<th>Title<th>Scenario<th>Players<th>Description<th>Changed<th>Active<th>
-<% if (active_games.length > 0) { %>
-<% active_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><a href="/info/<%= row.title_id %>"><%= row.title_name %></a>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.mtime %>
-<% if (row.is_active) { %>
-<td class="is_active"><%= row.active %>
-<% } else { %>
-<td><%= row.active %>
-<% } %>
-<% if (row.is_yours) { %>
-<% if (row.is_shared) { %>
-<td class="command"><a href="/join/<%= row.game_id %>">View</a>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>:<%- row.your_role %>">View</a>
-<% } %>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>">View</a>
-<% } %>
-<% }); %>
-<% } else { %>
-<tr><td colspan="8">No active games.
-<% } %>
-</table>
diff --git a/views/games.pug b/views/games.pug
new file mode 100644
index 0000000..2ab54f1
--- /dev/null
+++ b/views/games.pug
@@ -0,0 +1,18 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Public Games
+ if user
+ meta(http-equiv="refresh" content=300)
+ body
+ include header
+ article
+ h1 Public Games
+
+ h2 Open
+ +gametable(0, open_games)
+
+ h2 Active
+ +gametable(1, active_games)
diff --git a/views/head.pug b/views/head.pug
new file mode 100644
index 0000000..ae6bf30
--- /dev/null
+++ b/views/head.pug
@@ -0,0 +1,67 @@
+//- vim:ts=4:sw=4:
+
+meta(name="viewport" content="width=device-width,height=device-height,initial-scale=1")
+link(rel="icon" href="/favicon.svg")
+link(rel="stylesheet" href="/fonts/fonts.css")
+link(rel="stylesheet" href="/style.css")
+
+mixin gametable(status,table,hide_title=0)
+ table
+ tr
+ th ID
+ unless hide_title
+ th Title
+ th Scenario
+ th Players
+ th Description
+ case status
+ when 0
+ th Created
+ when 1
+ th Changed
+ th Turn
+ when 2
+ th Finished
+ th Result
+ th
+ each row in table
+ tr
+ td= row.game_id
+ unless hide_title
+ td.w: a(href="/info/"+row.title_id)= row.title_name
+ td.w= row.scenario
+ td!= row.player_names
+ td= row.description
+ case status
+ when 0
+ td.w= row.ctime
+ when 1
+ td.w= row.mtime
+ if (row.is_active)
+ td.is_active= row.active
+ else
+ td= row.active
+ when 2
+ td.w= row.mtime
+ td= row.result
+ td.command
+ if status === 0
+ a(href="/join/"+row.game_id) Join
+ else
+ - let cmd = status === 1 ? "Play" : "View"
+ if row.is_yours
+ if row.is_shared
+ a(href="/join/"+row.game_id)= cmd
+ else
+ a(href=`/${row.title_id}/play:${row.game_id}:${row.your_role}`)= cmd
+ else
+ a(href=`/${row.title_id}/play:${row.game_id}`) View
+ else
+ tr
+ case status
+ when 0
+ td(colspan=7-hide_title) No open games.
+ when 1
+ td(colspan=8-hide_title) No active games.
+ when 2
+ td(colspan=8-hide_title) No finished games.
diff --git a/views/header.ejs b/views/header.ejs
deleted file mode 100644
index 66f0f2e..0000000
--- a/views/header.ejs
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
-<% if (typeof refresh !== 'undefined' && refresh > 0) { -%>
-<meta http-equiv="refresh" content="<%= refresh %>">
-<% } -%>
-<title><%= title %></title>
-<link rel="icon" href="/favicon.svg">
-<link rel="stylesheet" href="/fonts/fonts.css">
-<link rel="stylesheet" href="/style.css">
-</head>
-<body>
-<header>
-<a href="/"><img src="/images/rally-the-troops.svg" width="48" height="48"></a>
-<nav>
-<a href="/about">About</a>
-<a href="/forum">Forum</a>
-<% if (user) { -%>
-<% if (user.unread > 0) { -%>
-<a href="/inbox">Inbox (<%= unread %>)</a>
-<% } else { -%>
-<a href="/inbox">Inbox</a>
-<% } -%>
-<a href="/profile">Profile (<%= user.name %>)</a>
-<% } else { -%>
-<a href="/signup">Signup</a>
-<a href="/login">Login</a>
-<% } -%>
-</nav>
-</header>
-<article>
-<h1><%= title %></h1>
-<% if (typeof flash !== 'undefined' && flash.length > 0) { -%>
-<p class="error"><%= flash %></p>
-<% } -%>
diff --git a/views/header.pug b/views/header.pug
new file mode 100644
index 0000000..7eb357b
--- /dev/null
+++ b/views/header.pug
@@ -0,0 +1,15 @@
+header
+ a(href="/")
+ img(src="/images/rally-the-troops.svg" width=48 height=48)
+ nav
+ a(href="/about") About
+ a(href="/forum") Forum
+ if user
+ if user.unread > 0
+ a(href="/inbox") Inbox (#{user.unread})
+ else
+ a(href="/inbox") Inbox
+ a(href="/profile") Profile (#{user.name})
+ else
+ a(href="/signup") Signup
+ a(href="/login") Login
diff --git a/views/index.ejs b/views/index.ejs
deleted file mode 100644
index 1f23381..0000000
--- a/views/index.ejs
+++ /dev/null
@@ -1,29 +0,0 @@
-<%- include('header', { title: "Rally the Troops!" }) _%>
-<style>
-.list { display: flex; flex-wrap: wrap; justify-content: left; max-width: 800px; }
-.list a { margin: 1em; display: block; }
-.list img { box-shadow: 2px 2px 4px 0px rgba(0,0,0,0.5); height: 200px; }
-</style>
-
-<p>
-Rally the Troops! is a website where you can play historic games with other
-players.
-
-<p>
-Registration and use is free, and there are no ads.
-
-<div class="list">
-<%
- for (let t in titles) {
- let title = titles[t];
- if (!title.hidden) {
-_%>
-<a href="/info/<%- title.title_id %>"><img src="/<%- title.title_id %>/cover.jpg"></a>
-<%
- }
- }
-_%>
-</div>
-
-<p>
-Join the <a href="https://discord.gg/CBrTh8k84A">Discord</a> server to find players or report bugs.
diff --git a/views/index.pug b/views/index.pug
new file mode 100644
index 0000000..8ada2fc
--- /dev/null
+++ b/views/index.pug
@@ -0,0 +1,49 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Rally the Troops!
+ style.
+ div.list {
+ max-width: 800px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: left;
+ }
+ div.list div {
+ margin: 10px 5px;
+ text-align: center;
+ }
+ div.list a:not(:hover) {
+ color: black;
+ text-decoration: none;
+ }
+ div.list img {
+ box-shadow: 2px 2px 4px 0px rgba(0,0,0,0.5);
+ height: 200px;
+ margin: 5px 10px;
+ display: block;
+ }
+ body
+ include header
+ article
+ h1 Rally the Troops!
+ if flash
+ p.error= flash
+
+ p Rally the Troops! is a website where you can play historic games with other players.
+
+ p Registration and use is free, and there are no ads.
+
+ div.list
+ each title in titles
+ unless title.hidden
+ div
+ a(href="/info/"+title.title_id)
+ img(src="/"+title.title_id+"/cover.jpg")
+ | #{title.title_name}
+
+ p: a(href="/games") List of all open and active games.
+
+ p Join the #[a(href="https://discord.gg/CBrTh8k84A") Discord] server to find players or report bugs.
diff --git a/views/info.ejs b/views/info.ejs
deleted file mode 100644
index 49625ed..0000000
--- a/views/info.ejs
+++ /dev/null
@@ -1,80 +0,0 @@
-<%- include('header', { title: title.title_name, refresh: (user ? 300 : 0) }) -%>
-<img class="logo" src="/<%= title.title_id %>/cover.jpg">
-<%- include('../public/' + title.title_id + '/about.html') -%>
-<p>
-Read more about the game on
-<a href="https://boardgamegeek.com/boardgame/<%= title.bgg %>">boardgamegeek.com</a>.
-
-<h2>Open Games</h2>
-<table>
-<tr><th>ID<th>Scenario<th>Players<th>Description<th>Created<th>
-<% if (open_games.length > 0) { -%>
-<% open_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.ctime %>
-<td class="command"><a href="/join/<%= row.game_id %>">Join</a>
-<% }); } else { %>
-<tr><td colspan="6">No open games.
-<% } %>
-</table>
-
-<p>
-<a href="/create/<%= title.title_id %>">Create a new game</a>.
-
-<% if (active_games.length > 0) { %>
-<h2>Active Games</h2>
-<table class="game">
-<tr><th>ID<th>Scenario<th>Players<th>Description<th>Changed<th>Turn<th>
-<% active_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.mtime %>
-<% if (row.is_active) { %>
-<td class="is_active"><%= row.active %>
-<% } else { %>
-<td><%= row.active %>
-<% } %>
-<% if (row.is_yours) { %>
-<% if (row.is_shared) { %>
-<td class="command"><a href="/join/<%= row.game_id %>">Play</a>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>:<%- row.your_role %>">Play</a>
-<% } %>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>">View</a>
-<% } %>
-<% }); %>
-</table>
-<% } %>
-
-<% if (finished_games.length > 0) { %>
-<h2>Finished Games</h2>
-<table class="game">
-<tr><th>ID<th>Scenario<th>Players<th>Description<th>Finished<th>Result<th>
-<% finished_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.mtime %>
-<td><%= row.result %>
-<% if (row.is_yours) { %>
-<% if (row.is_shared) { %>
-<td class="command"><a href="/join/<%= row.game_id %>">View</a>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>:<%- row.your_role %>">View</a>
-<% } %>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>">View</a>
-<% } %>
-<% }); %>
-</table>
-<% } %>
diff --git a/views/info.pug b/views/info.pug
new file mode 100644
index 0000000..d3d7da2
--- /dev/null
+++ b/views/info.pug
@@ -0,0 +1,32 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= title.title_name
+ if user
+ meta(http-equiv="refresh" content=300)
+ body
+ include header
+ article
+ h1= title.title_name
+
+ img.logo(src="/"+title.title_id+"/cover.jpg")
+
+ | !{ about_html }
+
+ p Read more about the game on #[a(href="https://boardgamegeek.com/boardgame/"+title.bgg) boardgamegeek.com].
+
+ h2 Open games
+ +gametable(0,open_games,1)
+
+ p
+ a(href="/create/"+title.title_id) Create a new game
+
+ if active_games.length > 0
+ h2 Active games
+ +gametable(1,active_games,1)
+
+ if finished_games.length > 0
+ h2 Finished games
+ +gametable(2,finished_games,1)
diff --git a/views/join.ejs b/views/join.ejs
deleted file mode 100644
index e2c626f..0000000
--- a/views/join.ejs
+++ /dev/null
@@ -1,52 +0,0 @@
-<%- include('header', { title: game.title_name }) -%>
-<style>
-table { min-width: auto; }
-th, td { min-width: 10em; }
-a.red { text-decoration: none; color: brown; font-size: 14px; }
-td a { text-decoration: underline; color: blue; }
-.hide { display: none; }
-</style>
-<script>
-let game = <%- JSON.stringify(game) %>;
-let roles = <%- JSON.stringify(roles) %>;
-let players = <%- JSON.stringify(players) %>;
-let user_id = <%- user.user_id %>;
-let ready = <%- ready %>;
-</script>
-<script src="/join.js"></script>
-<p id="error" class="error"></p>
-
-<a href="/info/<%= game.title_id %>"><img class="logo" src="/<%= game.title_id %>/cover.jpg"></a>
-
-<div class="info">
-<p>
-Owner: <%= game.owner_name %>
-<% if (game.private) { %>
-(private)
-<% } %>
-<br>
-Scenario: <%= game.scenario %>
-<br>
-Options: <%= game.options %>
-
-<p>
-<%= game.description || "No description." %>
-
-<br clear=left>
-
-<table class="small">
-<tr>
-<% roles.forEach((role) => { %>
-<th class="command" id="role_<%= role.replace(/ /g, '_') %>_name"><%= role %></th>
-<% }); %>
-<tr>
-<% roles.forEach((role) => { %>
-<td class="command" id="role_<%= role.replace(/ /g, '_') %>">-</td>
-<% }); %>
-<tr>
-<td class="command" id="message" colspan="<%= roles.length %>">-</td>
-</table>
-
-<p>
-<button class="hide" id="delete_button" onclick="confirm_delete()">Delete</button>
-<button class="hide" id="start_button" onclick="javascript:send('/start/<%= game.game_id %>')" disabled>Start!</button>
diff --git a/views/join.pug b/views/join.pug
new file mode 100644
index 0000000..be95075
--- /dev/null
+++ b/views/join.pug
@@ -0,0 +1,58 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= game.title_name
+ style.
+ table { min-width: auto; }
+ th,td { min-width: 10em; }
+ table td a.red { text-decoration: none; color: brown; font-size: 14px; }
+ td a { text-decoration: underline; color: blue; }
+ .hide { display: none; }
+ script.
+ let game = !{ JSON.stringify(game) };
+ let roles = !{ JSON.stringify(roles) };
+ let players = !{ JSON.stringify(players) };
+ let user_id = !{ user.user_id };
+ let ready = !{ ready };
+ script(src="/join.js")
+ body
+ include header
+ article
+ h1= game.title_name
+ if flash
+ p.error#error= flash
+ else
+ p.error#error
+
+ a(href="/info/"+game.title_id): img.logo(src="/"+game.title_id+"/cover.jpg")
+
+ p
+ if game.private
+ | Owner: #{game.owner_name} (private)
+ else
+ | Owner: #{game.owner_name}
+ br
+ | Scenario: #{game.scenario}
+ br
+ | Options: #{game.options}
+
+ p= game.description || "No description."
+
+ br(clear="left")
+
+ p
+ table
+ tr
+ each role in roles
+ th.command(id="role_"+role.replace(/ /g, "_")+"_name")= role
+ tr
+ each role in roles
+ td.command(id="role_"+role.replace(/ /g, "_")) -
+ tr
+ td.command#message(colspan=roles.length) -
+
+ p
+ button.hide#delete_button(onclick="confirm_delete()") Delete
+ button.hide#start_button(onclick=`javascript:send('/start/${game.game_id}')` disabled) Start
diff --git a/views/login.ejs b/views/login.ejs
deleted file mode 100644
index 3e1dd43..0000000
--- a/views/login.ejs
+++ /dev/null
@@ -1,17 +0,0 @@
-<%- include('header', { title: "Login" }) %>
-<% if (user) { %>
-<p>You're already logged in!
-<% } else { %>
-<form action="/login" method="post">
-<p>
-<label for="username">Name or mail: </label><br>
-<input type="text" id="username" name="username" required>
-<p>
-<label for="password">Password: </label><br>
-<input type="password" id="password" name="password" required>
-<p>
-<button type="submit">Login</button>
-</form>
-<p>
-<a href="/forgot_password">Forgot password</a>
-<% } %>
diff --git a/views/login.pug b/views/login.pug
new file mode 100644
index 0000000..58a1fd7
--- /dev/null
+++ b/views/login.pug
@@ -0,0 +1,29 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Login
+ body
+ include header
+ article
+ h1 Login
+ if flash
+ p.error= flash
+
+ if user
+ p You're already logged in!
+ else
+ form(method="post" action="/login")
+ p
+ label Name or mail:
+ br
+ input(type="text" id="username" name="username" required)
+ p
+ label Password:
+ br
+ input(type="password" id="password" name="password" required)
+ p
+ button(type="submit") Login
+ p
+ a(href="/forgot_password") Forgot password
diff --git a/views/message_inbox.ejs b/views/message_inbox.ejs
deleted file mode 100644
index 626662d..0000000
--- a/views/message_inbox.ejs
+++ /dev/null
@@ -1,21 +0,0 @@
-<%- include('header', { title: "Inbox" }) -%>
-<style>
-.unread { background-color: lightyellow; }
-</style>
-<p><a href="/message/send">Send message</a>
-<table class="post">
-<tr><th>From<th>Subject<th>Date
-<% if (messages.length > 0) { messages.forEach((row) => { %>
-<% if (row.read) { %>
-<tr class="read">
-<% } else {%>
-<tr>
-<% } %>
-<td><a href="/user/<%- row.from_name %>"><%= row.from_name %></a>
-<td><a href="/message/read/<%- row.message_id %>"><%= row.subject %></a>
-<td><%= row.time %>
-<% }); } else { %>
-<tr><td colspan="3">No messages</td>
-<% } %>
-</table>
-<p><a href="/outbox">Outbox</a>
diff --git a/views/message_inbox.pug b/views/message_inbox.pug
new file mode 100644
index 0000000..28563d8
--- /dev/null
+++ b/views/message_inbox.pug
@@ -0,0 +1,31 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Inbox
+ style .unread { background-color: lightyellow }
+ body
+ include header
+ article
+ h1 Inbox
+
+ p
+ a(href="/message/send") Send message
+
+ table
+ tr
+ th From
+ th Subject
+ th Date
+ each row in messages
+ tr(class=row.read?"read":"unread")
+ td: a(href="/user/"+row.from_name)= row.from_name
+ td: a(href="/message/read/"+row.message_id)= row.subject
+ td= row.time
+ else
+ tr
+ td(colspan=3) No messages.
+
+ p
+ a(href="/outbox") Outbox
diff --git a/views/message_outbox.ejs b/views/message_outbox.ejs
deleted file mode 100644
index a5d1abd..0000000
--- a/views/message_outbox.ejs
+++ /dev/null
@@ -1,28 +0,0 @@
-<%- include('header', { title: "Outbox" }) -%>
-<style>
-.unread { background-color: lightyellow; }
-</style>
-<script>
-function delete_all() {
- let warning = "Are you sure you want to delete ALL the messages?";
- if (window.confirm(warning))
- window.location.href = "/outbox/delete";
-}
-</script>
-<table class="post">
-<tr><th>To<th>Subject<th>Date
-<% if (messages.length > 0) { messages.forEach((row) => { %>
-<% if (row.read) { %>
-<tr class="read">
-<% } else {%>
-<tr>
-<% } %>
-<td><a href="/user/<%- row.to_name %>"><%= row.to_name %></a>
-<td><a href="/message/read/<%- row.message_id %>"><%= row.subject %></a>
-<td><%= row.time %>
-<% }); } else { %>
-<tr><td colspan="3">No messages</td>
-<% } %>
-</table>
-<p>
-<button onclick="delete_all()">Delete all</button>
diff --git a/views/message_outbox.pug b/views/message_outbox.pug
new file mode 100644
index 0000000..ee19a2f
--- /dev/null
+++ b/views/message_outbox.pug
@@ -0,0 +1,36 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Outbox
+ script.
+ function delete_all() {
+ let warning = "Are you sure you want to delete ALL the messages?";
+ if (window.confirm(warning))
+ window.location.href = "/outbox/delete";
+ }
+ body
+ include header
+ article
+ h1 Outbox
+
+ p
+ a(href="/message/send") Send message
+
+ table
+ tr
+ th From
+ th Subject
+ th Date
+ each row in messages
+ tr
+ td: a(href="/user/"+row.to_name)= row.to_name
+ td: a(href="/message/read/"+row.message_id)= row.subject
+ td= row.time
+ else
+ tr
+ td(colspan=3) No messages.
+
+ p
+ button(onclick="delete_all()") Delete all
diff --git a/views/message_read.ejs b/views/message_read.ejs
deleted file mode 100644
index b2f8227..0000000
--- a/views/message_read.ejs
+++ /dev/null
@@ -1,25 +0,0 @@
-<%- include('header', { title: message.subject }) %>
-<style>
-th{font-weight:normal;width:4em;}
-</style>
-<script>
-function delete_message(id) {
- let warning = "Are you sure you want to DELETE this message?";
- if (window.confirm(warning))
- window.location.href = "/message/delete/" + id;
-}
-function reply_message(id) {
- window.location.href = "/message/reply/" + id;
-}
-</script>
-<table>
-<tr><th>From:<td><a href="/user/<%- message.from_name %>"><%= message.from_name %></a>
-<tr><th>To:<td><a href="/user/<%- message.to_name %>"><%= message.to_name %></a>
-<tr><th>Date:<td><%= message.time %>
-<tr><td colspan="2" class="body"><%- message.body %></td>
-</table>
-<p>
-<% if ( 1 || message.from_id !== user.user_id ) { %>
-<button onclick="reply_message(<%- message.message_id %>)">Reply</button>
-<% } %>
-<button onclick="delete_message(<%- message.message_id %>)">Delete</button>
diff --git a/views/message_read.pug b/views/message_read.pug
new file mode 100644
index 0000000..65b845d
--- /dev/null
+++ b/views/message_read.pug
@@ -0,0 +1,38 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= message.subject
+ style th { font-weight: normal; width: 4em; }
+ script.
+ function delete_message(id) {
+ let warning = "Are you sure you want to DELETE this message?";
+ if (window.confirm(warning))
+ window.location.href = "/message/delete/" + id;
+ }
+ function reply_message(id) {
+ window.location.href = "/message/reply/" + id;
+ }
+ body
+ include header
+ article
+ h1= message.subject
+
+ table
+ tr
+ th From:
+ td: a(href="/user/"+message.from_name)= message.from_name
+ tr
+ th To:
+ td: a(href="/user/"+message.to_name)= message.to_name
+ tr
+ th Date:
+ td= message.time
+ tr
+ td.body(colspan=2)!= message.body
+
+ p
+ if message.from_id !== user.user_id
+ button(onclick="reply_message("+message.message_id+")") Reply
+ button(onclick="delete_message("+message.message_id+")") Delete
diff --git a/views/message_send.ejs b/views/message_send.ejs
deleted file mode 100644
index cea21e3..0000000
--- a/views/message_send.ejs
+++ /dev/null
@@ -1,33 +0,0 @@
-<%- include('header', { title: "Send Message" }) -%>
-<style>
-input, textarea { width: 100%; max-width: 45rem; }
-</style>
-<form action="/message/send" method="post">
-<p>
-To:<br>
-<input id="to" type="text" name="to" size="80" maxlength="80"
- onkeypress="if(event.keyCode===13){document.querySelector('#subject').focus();return false;}"
- value="<%= to_name %>"
- <%= (to_name === "") ? "autofocus" : "" %>
- required>
-
-<p>
-Subject:
-<br>
-<input id="subject" type="text" name="subject" size="80" maxlength="80"
- onkeypress="if(event.keyCode===13){document.querySelector('#body').focus();return false;}"
- value="<%= subject %>"
- <%= (to_name !== "" && subject === "") ? "autofocus" : "" %>
- pattern=".*\S+.*"
- required>
-
-<p>
-Body:
-<br>
-<textarea id="body" name="body" rows="20" cols="80" maxlength="32000"
- <%= (to_name !== "" && subject !== "") ? "autofocus" : "" %>
- required>
-<%= body %></textarea>
-<p>
-<button type="submit">Send</button>
-</form>
diff --git a/views/message_send.pug b/views/message_send.pug
new file mode 100644
index 0000000..c573ef6
--- /dev/null
+++ b/views/message_send.pug
@@ -0,0 +1,68 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Send Message
+ style.
+ input, textarea { width: min(45rem,100%) }
+ script.
+ function next(event,sel) {
+ if (event.keyCode === 13) {
+ document.querySelector(sel).focus();
+ return false;
+ }
+ return true;
+ }
+
+ body
+ include header
+ article
+ h1 Send Message
+ form(method="post" action="/message/send")
+
+ p To:
+ br
+ input(
+ id="to"
+ type="text"
+ name="to"
+ required
+ size=80
+ maxlength=80
+ value=to_name
+ onpress="return next(event,'#subject')"
+ autofocus=(to_name === "")
+ )
+
+ p Subject:
+ br
+ input(
+ id="subject"
+ type="text"
+ name="subject"
+ required
+ size=80
+ maxlength=80
+ pattern=".*\\S+.*"
+ value=subject
+ onpress="return next(event,'#body')"
+ autofocus=(to_name !== "" && subject === "")
+ )
+
+ p Body:
+ br
+ textarea(
+ id="body"
+ name="body"
+ required
+ cols=80
+ rows=20
+ maxlength=32000
+ autofocus=(to_name !== "" && subject !== "")
+ )
+ |
+ | #{body}
+
+ p
+ button(type="submit") Send
diff --git a/views/profile.ejs b/views/profile.ejs
deleted file mode 100644
index a4a23fc..0000000
--- a/views/profile.ejs
+++ /dev/null
@@ -1,102 +0,0 @@
-<%- include('header', { title: "Rally the Troops!", refresh: (active_games.length > 0 ? 300 : 0) }) -%>
-<img class="avatar" src="<%= avatar %>" width="80" height="80">
-<p>
-Welcome, <%= user.name %>!
-<p>
-Your mail address is <%= user.mail %>.
-
-<br clear=left>
-
-<p>&#xbb;
-<% if (user.notify) { %>
-<a href="/unsubscribe">Disable mail notifications</a>
-<% } else { %>
-<a href="/subscribe">Enable mail notifications</a>
-<% } %>
-<br>&#xbb;
-Change
-<a href="/change_password">password</a>,
-<a href="/change_mail">mail address</a>,
-<a href="/change_name">name</a>,
-or <a href="/change_about">profile text</a>.
-<br>&#xbb;
-<a href="/chat">Chat log</a>
-<br>&#xbb;
-<a href="/logout">Logout</a>
-
-<% if (open_games.length > 0) { %>
-<h2>Open Games</h2>
-<table>
-<thead>
-<tr><th>ID<th>Game<th>Scenario<th>Players<th>Description<th>Created<th>
-<tbody>
-<% open_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><a href="/info/<%= row.title_id %>"><%= row.title_name %></a>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.ctime %>
-<td class="command"><a href="/join/<%= row.game_id %>">Join</a>
-<% }); %>
-</table>
-<% } %>
-
-<% if (active_games.length > 0) { %>
-<h2>Active Games</h2>
-<table class="game">
-<thead>
-<tr><th>ID<th>Game<th>Scenario<th>Players<th>Description<th>Changed<th>Turn<th>
-<tbody>
-<% active_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><a href="/info/<%= row.title_id %>"><%= row.title_name %></a>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.mtime %>
-<% if (row.is_active) { %>
-<td class="is_active"><%= row.active %>
-<% } else { %>
-<td><%= row.active %>
-<% } %>
-<% if (row.is_shared) { %>
-<td class="command"><a href="/join/
-<%= row.game_id %>">Play</a>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>:<%- row.your_role %>">Play</a>
-<% } %>
-<% }); %>
-</table>
-<% } %>
-
-<% if (finished_games.length > 0) { %>
-<h2>Finished Games</h2>
-<table>
-<thead>
-<tr><th>ID<th>Game<th>Scenario<th>Players<th>Description<th>Finished<th>Result<th>
-<tbody>
-<% finished_games.forEach((row) => { %>
-<tr>
-<td><%= row.game_id %>
-<td class="w"><a href="/info/<%= row.title_id %>"><%= row.title_name %></a>
-<td class="w"><%= row.scenario %>
-<td><%- row.player_names %>
-<td><%= row.description %>
-<td class="w"><%= row.mtime %>
-<td><%= row.result %>
-<% if (row.is_shared) { %>
-<td class="command"><a href="/join/<%= row.game_id %>">View</a>
-<% } else { %>
-<td class="command"><a href="/<%- row.title_id %>/play:<%- row.game_id %>:<%- row.your_role %>">View</a>
-<% } %>
-<% }); %>
-</table>
-<% } %>
-
-<% if (open_games.length === 0 && active_games.length === 0 && finished_games.length === 0) { %>
-<p>
-You don't have any current or finished games.
-<% } %>
diff --git a/views/profile.pug b/views/profile.pug
new file mode 100644
index 0000000..e43a999
--- /dev/null
+++ b/views/profile.pug
@@ -0,0 +1,49 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Rally the Troops!
+ if active_games.length > 0
+ meta(http-equiv="refresh" content=300)
+ body
+ include header
+ article
+ h1 Rally the Troops!
+ a(href="https://gravatar.com/"): img.avatar(src=avatar)
+
+ p Welcome, #{user.name}!
+ p Your mail address is #{user.mail}
+ br(clear="left")
+
+ p
+ if user.notify
+ | &#xbb; <a href="/unsubscribe">Disable mail notifications</a>
+ else
+ | &#xbb; <a href="/subscribe">Enable mail notifications</a>
+ br
+ | &#xbb;
+ | Change
+ | <a href="/change_password">password</a>,
+ | <a href="/change_mail">mail address</a>,
+ | <a href="/change_name">name</a>,
+ | or <a href="/change_about">profile text</a>.
+ br
+ | &#xbb; <a href="/chat">Chat log</a>
+ br
+ | &#xbb; <a href="/logout">Logout</a>
+
+ if open_games.length > 0
+ h2 Open games
+ +gametable(0,open_games)
+
+ if active_games.length > 0
+ h2 Active games
+ +gametable(1,active_games)
+
+ if finished_games.length > 0
+ h2 Finished games
+ +gametable(2,finished_games)
+
+ if open_games.length === 0 && active_games.length === 0 && finished_games.length === 0
+ p You don't have any current or finished games.
diff --git a/views/reset_password.ejs b/views/reset_password.ejs
deleted file mode 100644
index 8920da7..0000000
--- a/views/reset_password.ejs
+++ /dev/null
@@ -1,14 +0,0 @@
-<%- include('header', { title: "Reset password" }) %>
-<form action="/reset_password" method="post">
-<p>
-<label for="mail">Mail: </label><br>
-<input type="text" id="mail" name="mail" size="32" value="<%= mail %>" required>
-<p>
-<label for="password">New Password: </label><br>
-<input type="password" id="password" name="password" size="32" required>
-<p>
-<label for="token">Token: </label><br>
-<input type="text" id="token" name="token" size="32" value="<%= token %>" style="font-family:monospace" required>
-<p>
-<button type="submit">Reset password</button>
-</form>
diff --git a/views/reset_password.pug b/views/reset_password.pug
new file mode 100644
index 0000000..856433e
--- /dev/null
+++ b/views/reset_password.pug
@@ -0,0 +1,28 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Reset password
+ body
+ include header
+ article
+ h1 Reset password
+ if flash
+ p.error= flash
+
+ form(method="post" action="/reset_password")
+ p
+ label Mail:
+ br
+ input(type="text" name="mail" size=32 value=mail required)
+ p
+ label New Password:
+ br
+ input(type="password" name="password" size=32 required)
+ p
+ label Token:
+ br
+ input(type="text" name="token" size=32 value=token style="font-family:monospace" required)
+ p
+ button(type="submit") Reset password
diff --git a/views/signup.ejs b/views/signup.ejs
deleted file mode 100644
index 819da3f..0000000
--- a/views/signup.ejs
+++ /dev/null
@@ -1,18 +0,0 @@
-<%- include('header', { title: "Signup" }) %>
-<% if (user) { %>
-<p>You're already logged in!
-<% } else { %>
-<form action="/signup" method="post">
-<p>
-<label for="username">Name: </label><br>
-<input type="text" id="username" name="username" required>
-<p>
-<label for="mail">Mail: </label><br>
-<input type="text" id="mail" name="mail" required>
-<p>
-<label for="password">Password: </label><br>
-<input type="password" id="password" name="password" required>
-<p>
-<button type="submit">Create Account</button>
-</form>
-<% } %>
diff --git a/views/signup.pug b/views/signup.pug
new file mode 100644
index 0000000..e46007a
--- /dev/null
+++ b/views/signup.pug
@@ -0,0 +1,31 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Signup
+ body
+ include header
+ article
+ h1 Signup
+ if flash
+ p.error= flash
+
+ if user
+ p You're already logged in!
+ else
+ form(method="post" action="/signup")
+ p
+ label Name:
+ br
+ input(type="text" id="username" name="username" required)
+ p
+ label Mail:
+ br
+ input(type="text" id="mail" name="mail" required)
+ p
+ label Password:
+ br
+ input(type="password" id="password" name="password" required)
+ p
+ button(type="submit") Create account
diff --git a/views/stats.ejs b/views/stats.ejs
deleted file mode 100644
index 95b52d2..0000000
--- a/views/stats.ejs
+++ /dev/null
@@ -1,39 +0,0 @@
-<%- include('header', { title: "Game Statistics" }) -%>
-<style>tr.blank{height:2rem;border:none;}</style>
-<table class="wide">
-<%
- function total(t, s) {
- return stats
- .filter(entry => entry.title_id === t && entry.scenario === s)
- .reduce((acc, entry) => acc + entry.count, 0);
- }
- function result(t, s, r) {
- let info = stats.find(entry => {
- return entry.title_id === t &&
- entry.scenario === s &&
- entry.result === r});
- return info ? info.count : 0;
- }
- for (let title_id in title_name_map) {
- if (title_name_map[title_id].hidden)
- continue;
- let scenarios = title_rule_map[title_id].scenarios;
- let roles = title_role_map[title_id].concat(['Draw']);
- %><tr><th><%= title_name_map[title_id].title_name %><%
- roles.forEach(role => {
- %><th><%= role %><%
- });
- scenarios.forEach(scenario => {
- let t = total(title_id, scenario);
- if (t > 0) {
- %><tr><td><%= scenario %> (<%= t %>)<%
- roles.forEach(role => {
- let r = result(title_id, scenario, role);
- %><td><%= Math.round(r * 100 / t) %>%<%
- });
- }
- });
- %><tr class="blank"><%
- }
-%>
-</table>
diff --git a/views/stats.pug b/views/stats.pug
new file mode 100644
index 0000000..7634048
--- /dev/null
+++ b/views/stats.pug
@@ -0,0 +1,45 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title Game Statistics
+ style.
+ table { table-layout: fixed; min-width: auto; }
+ td:nth-child(1) { width: 240px; }
+ td { width: 100px; }
+ tr.blank { height: 2rem; border: none; }
+ body
+ include header
+ article
+ h1 Game Statistics
+ table
+ -
+ function total(t,s) {
+ return stats
+ .filter(entry => entry.title_id === t && entry.scenario === s)
+ .reduce((acc, entry) => acc + entry.count, 0);
+ }
+ function result(t,s,r) {
+ let info = stats.find(entry => entry.title_id === t && entry.scenario === s && entry.result === r);
+ return info ? info.count : 0;
+ }
+ each title_name, title_id in title_name_map
+ unless title_name_map[title_id].hidden
+ - let scenarios = title_rule_map[title_id].scenarios;
+ - let roles = title_role_map[title_id].concat(["Draw"]);
+ tr
+ th= title_name_map[title_id].title_name
+ each role in roles
+ th= role
+ each scenario in scenarios
+ - let t = total(title_id, scenario);
+ tr
+ td #{scenario} (#{t})
+ each role in roles
+ if t > 0
+ -let r = result(title_id, scenario, role);
+ td= Math.round(r * 100 / t) + "%"
+ else
+ td -
+ tr.blank
diff --git a/views/user.ejs b/views/user.ejs
deleted file mode 100644
index 8fd1d08..0000000
--- a/views/user.ejs
+++ /dev/null
@@ -1,15 +0,0 @@
-<%- include('header', { title: who.name }) %>
-<img class="avatar" src="<%= who.avatar %>" width="80" height="80">
-<% if (who.about) { %>
-<p style="white-space:pre-wrap;font-style:italic"><%= who.about %></p>
-<% } else { %>
-<br clear="left">
-<% } %>
-<p>
-Member since <%= who.ctime %>.
-<p>
-Last seen <%= who.atime %>.
-<% if (user) { %>
-<p>
-<a href="/message/send/<%- who.name %>">Send message</a>
-<% } %>
diff --git a/views/user.pug b/views/user.pug
new file mode 100644
index 0000000..99420cc
--- /dev/null
+++ b/views/user.pug
@@ -0,0 +1,23 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title= who.name
+ body
+ include header
+ article
+ h1= who.name
+
+ img.avatar(src=who.avatar width=80 height=80)
+
+ if who.about
+ p(style="white-space:pre-wrap;font-style:italic")= who.about
+ else
+ br(clear="left")
+
+ p Member since #{who.ctime}.
+ p Last seen #{who.atime}.
+ if user
+ p
+ a(href="/message/send/"+who.name) Send message
diff --git a/views/user_list.pug b/views/user_list.pug
new file mode 100644
index 0000000..b4383f7
--- /dev/null
+++ b/views/user_list.pug
@@ -0,0 +1,26 @@
+//- vim:ts=4:sw=4:
+doctype html
+html
+ head
+ include head
+ title User List
+ style.
+ td.avatar { padding: 0; width: 80px; }
+ td.avatar img { display: block; width: 80px; height: 80px; }
+ body
+ include header
+ article
+ h1 User List
+
+ table
+ tr
+ th Avatar
+ th Name
+ th Member since
+ th Last seen
+ each row in user_list
+ tr
+ td.avatar: img(src=row.avatar)
+ td.name: a(href="/user/"+row.name)= row.name
+ td= row.ctime
+ td= row.atime
diff --git a/views/users.ejs b/views/users.ejs
deleted file mode 100644
index 9255d04..0000000
--- a/views/users.ejs
+++ /dev/null
@@ -1,14 +0,0 @@
-<%- include('header', { title: "User list" }) %>
-<style>
-td.avatar{padding:0;width:80px;}
-td.avatar img{display:block;width:80px;height:80px;}
-</style>
-<table class="post">
-<tr><th>Avatar<th>Name<th>Member since<th>Last seen
-<% userList.forEach((row) => { %>
-<tr>
-<td class="avatar"><img src="<%= row.avatar %>">
-<td class="name"><a href="/user/<%= row.name %>"><%= row.name %></a>
-<td><%= row.ctime %>
-<td><%= row.atime %>
-<% }); %>