first commit
This commit is contained in:
280
web/static/css/style.css
Normal file
280
web/static/css/style.css
Normal file
@@ -0,0 +1,280 @@
|
||||
/* Custom styles for Homework Manager */
|
||||
|
||||
:root {
|
||||
--primary-color: #4361ee;
|
||||
--secondary-color: #3f37c9;
|
||||
--success-color: #4cc9f0;
|
||||
--warning-color: #f72585;
|
||||
--danger-color: #e63946;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f8f9fa;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-weight: bold;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Navbar customization */
|
||||
.navbar {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Table improvements */
|
||||
.table {
|
||||
background-color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(67, 97, 238, 0.05);
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Badge styles */
|
||||
.badge {
|
||||
font-weight: 500;
|
||||
padding: 0.4em 0.8em;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(67, 97, 238, 0.25);
|
||||
}
|
||||
|
||||
/* Alert enhancements */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Stats cards */
|
||||
.card.bg-primary,
|
||||
.card.bg-warning,
|
||||
.card.bg-info,
|
||||
.card.bg-danger {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.card.bg-primary:hover,
|
||||
.card.bg-warning:hover,
|
||||
.card.bg-info:hover,
|
||||
.card.bg-danger:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Tabs - Removed conflicted specific styles as they are managed in templates */
|
||||
.nav-tabs .nav-link {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Login/Register cards */
|
||||
.card.shadow {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
/* Empty states */
|
||||
.text-muted.display-1 {
|
||||
color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table td,
|
||||
.table th {
|
||||
padding: 0.35rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Animations for Anxiety/Urgency */
|
||||
@keyframes pulse-bg {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #ffe69c;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-bg-danger {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #f5c2c7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-text {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.anxiety-warning {
|
||||
animation: pulse-bg 2s infinite;
|
||||
}
|
||||
|
||||
.anxiety-danger {
|
||||
animation: pulse-bg-danger 1s infinite;
|
||||
}
|
||||
|
||||
/* Custom Table Styles - Compact */
|
||||
.custom-table thead th {
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
font-size: 0.8rem;
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.custom-table tbody td {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
vertical-align: middle;
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.custom-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Compact form elements */
|
||||
.form-control-custom,
|
||||
.form-select-custom,
|
||||
.btn-custom {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Custom Tab Styles */
|
||||
.nav-tabs .nav-link {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: #6c757d;
|
||||
/* text-muted */
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #000 !important;
|
||||
font-weight: 700;
|
||||
border-bottom: 3px solid #000 !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Status Icon Size */
|
||||
.bi-circle,
|
||||
.bi-check-circle-fill {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Urgency styles for countdown */
|
||||
.countdown-urgent {
|
||||
color: #dc3545;
|
||||
font-weight: 700;
|
||||
animation: blink-text 1s infinite;
|
||||
}
|
||||
|
||||
.countdown-warning {
|
||||
color: #fd7e14;
|
||||
font-weight: 700;
|
||||
}
|
||||
33
web/static/js/app.js
Normal file
33
web/static/js/app.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Homework Manager JavaScript
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
const alerts = document.querySelectorAll('.alert:not(.alert-danger)');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
alert.classList.add('fade');
|
||||
setTimeout(function() {
|
||||
alert.remove();
|
||||
}, 150);
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Confirm dialogs for dangerous actions
|
||||
const confirmForms = document.querySelectorAll('form[data-confirm]');
|
||||
confirmForms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!confirm(form.dataset.confirm)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set default datetime to now + 1 day for new assignments
|
||||
const dueDateInput = document.getElementById('due_date');
|
||||
if (dueDateInput && !dueDateInput.value) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(23, 59, 0, 0);
|
||||
dueDateInput.value = tomorrow.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
115
web/templates/admin/api_keys.html
Normal file
115
web/templates/admin/api_keys.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1 class="mb-4"><i class="bi bi-key me-2"></i>APIキー管理</h1>
|
||||
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
|
||||
{{if .newKey}}
|
||||
<div class="alert alert-success">
|
||||
<h5 class="alert-heading"><i class="bi bi-check-circle me-2"></i>APIキーが作成されました</h5>
|
||||
<p class="mb-2">キー名: <strong>{{.newKeyName}}</strong></p>
|
||||
<p class="mb-0">以下のキーを安全な場所に保存してください。このキーは二度と表示されません。</p>
|
||||
<hr>
|
||||
<div class="d-flex align-items-center">
|
||||
<code class="flex-grow-1 bg-dark text-light p-2 rounded me-2" id="newApiKey">{{.newKey}}</code>
|
||||
<button class="btn btn-outline-secondary" onclick="copyKey()"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle me-2"></i>新規APIキー作成
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/admin/api-keys" method="POST" class="row g-3">
|
||||
{{.csrfField}}
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="name" placeholder="キー名(例: 外部連携用)" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-plus me-1"></i>作成</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .apiKeys}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>キー名</th>
|
||||
<th>作成者</th>
|
||||
<th>最終使用</th>
|
||||
<th>作成日</th>
|
||||
<th style="width: 100px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .apiKeys}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td><i class="bi bi-key me-1"></i>{{.Name}}</td>
|
||||
<td>{{if .User}}{{.User.Name}}{{else}}-{{end}}</td>
|
||||
<td>{{if .LastUsed}}{{formatDateTime .LastUsed}}{{else}}<span class="text-muted">未使用</span>{{end}}</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
<td>
|
||||
<form action="/admin/api-keys/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('このAPIキーを削除しますか?')">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="削除"><i
|
||||
class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-key display-1 text-muted"></i>
|
||||
<h3 class="mt-3">APIキーがありません</h3>
|
||||
<p class="text-muted">上のフォームから新しいAPIキーを作成してください。</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle me-2"></i>API使用方法
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2">APIにアクセスするには、<code>Authorization</code>ヘッダーにAPIキーを設定してください:</p>
|
||||
<pre
|
||||
class="bg-dark text-light p-3 rounded"><code>curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/v1/assignments</code></pre>
|
||||
<h6 class="mt-3">利用可能なエンドポイント:</h6>
|
||||
<ul class="mb-0">
|
||||
<li><code>GET /api/v1/assignments</code> - 課題一覧取得</li>
|
||||
<li><code>GET /api/v1/assignments/pending</code> - 未完了の課題一覧</li>
|
||||
<li><code>GET /api/v1/assignments/completed</code> - 完了済みの課題一覧</li>
|
||||
<li><code>GET /api/v1/assignments/overdue</code> - 期限切れの課題一覧</li>
|
||||
<li><code>GET /api/v1/assignments/due-today</code> - 今日が期限の課題一覧</li>
|
||||
<li><code>GET /api/v1/assignments/due-this-week</code> - 今週中が期限の課題一覧</li>
|
||||
<li><code>GET /api/v1/assignments/:id</code> - 課題詳細取得</li>
|
||||
<li><code>POST /api/v1/assignments</code> - 課題作成</li>
|
||||
<li><code>PUT /api/v1/assignments/:id</code> - 課題更新</li>
|
||||
<li><code>DELETE /api/v1/assignments/:id</code> - 課題削除</li>
|
||||
<li><code>PATCH /api/v1/assignments/:id/toggle</code> - 完了状態切替</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
function copyKey() {
|
||||
const key = document.getElementById('newApiKey').innerText;
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
alert('APIキーをコピーしました');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
65
web/templates/admin/users.html
Normal file
65
web/templates/admin/users.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1 class="mb-4"><i class="bi bi-people me-2"></i>ユーザー管理</h1>
|
||||
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
|
||||
{{if .users}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名前</th>
|
||||
<th>メールアドレス</th>
|
||||
<th>ロール</th>
|
||||
<th>登録日</th>
|
||||
<th style="width: 200px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .users}}
|
||||
<tr {{if eq .ID $.currentUserID}}class="table-primary" {{end}}>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.Name}}{{if eq .ID $.currentUserID}}<span class="badge bg-info ms-2">自分</span>{{end}}</td>
|
||||
<td>{{.Email}}</td>
|
||||
<td>{{if eq .Role "admin"}}<span class="badge bg-danger">管理者</span>{{else}}<span
|
||||
class="badge bg-secondary">ユーザー</span>{{end}}</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
<td>
|
||||
{{if ne .ID $.currentUserID}}
|
||||
<form action="/admin/users/{{.ID}}/role" method="POST" class="d-inline" {{if eq .Role "admin"
|
||||
}}onsubmit="return confirm('このユーザーを一般ユーザーに降格しますか?')"
|
||||
{{else}}onsubmit="return confirm('このユーザーを管理者に昇格しますか?')" {{end}}>
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
{{if eq .Role "admin"}}
|
||||
<input type="hidden" name="role" value="user">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" title="ユーザーに降格"><i
|
||||
class="bi bi-arrow-down"></i></button>
|
||||
{{else}}
|
||||
<input type="hidden" name="role" value="admin">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="管理者に昇格"><i
|
||||
class="bi bi-arrow-up"></i></button>
|
||||
{{end}}
|
||||
</form>
|
||||
<form action="/admin/users/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('このユーザーを削除しますか?')">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="削除"><i
|
||||
class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
{{else}}<span class="text-muted">-</span>{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-people display-1 text-muted"></i>
|
||||
<h3 class="mt-3">ユーザーがいません</h3>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
51
web/templates/assignments/edit.html
Normal file
51
web/templates/assignments/edit.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-pencil me-2"></i>課題編集</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
<form method="POST" action="/assignments/{{.assignment.ID}}">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.assignment.Title}}"
|
||||
required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">科目</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject"
|
||||
value="{{.assignment.Subject}}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="priority" class="form-label">重要度</label>
|
||||
<select class="form-select" id="priority" name="priority">
|
||||
<option value="low" {{if eq .assignment.Priority "low" }}selected{{end}}>小</option>
|
||||
<option value="medium" {{if eq .assignment.Priority "medium" }}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .assignment.Priority "high" }}selected{{end}}>大</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date"
|
||||
value="{{formatDateInput .assignment.DueDate}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">説明</label>
|
||||
<textarea class="form-control" id="description" name="description"
|
||||
rows="3">{{.assignment.Description}}</textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>更新</button>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">キャンセル</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
270
web/templates/assignments/index.html
Normal file
270
web/templates/assignments/index.html
Normal file
@@ -0,0 +1,270 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="mb-0 fw-bold"><i class="bi bi-list-task me-2"></i>課題一覧</h4>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-secondary text-white" onclick="toggleCountdown()" id="toggleCountdownBtn">
|
||||
<i class="bi bi-clock me-1"></i><span id="countdownBtnText">カウントダウン表示中</span>
|
||||
</button>
|
||||
<a href="/assignments/new" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-lg me-1"></i>新規登録
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs border-0 mb-2" id="assignmentTabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 border-0 {{if eq .filter " pending"}}fw-bold border-bottom border-dark
|
||||
border-3{{else}}text-muted{{end}}" href="/assignments?filter=pending&q={{.query}}&priority={{.priority}}"
|
||||
style="{{if eq .filter " pending"}}color: black !important;{{end}}">未完了</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 border-0 {{if eq .filter " completed"}}fw-bold border-bottom border-dark
|
||||
border-3{{else}}text-muted{{end}}" href="/assignments?filter=completed&q={{.query}}&priority={{.priority}}"
|
||||
style="{{if eq .filter " completed"}}color: black !important;{{end}}">完了済み</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 border-0 {{if eq .filter " overdue"}}fw-bold border-bottom border-dark
|
||||
border-3{{else}}text-muted{{end}}" href="/assignments?filter=overdue&q={{.query}}&priority={{.priority}}"
|
||||
style="{{if eq .filter " overdue"}}color: black !important;{{end}}">期限切れ</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr class="mt-0 mb-3 text-muted" style="opacity: 0.1;">
|
||||
|
||||
<!-- Filter Section -->
|
||||
<form action="/assignments" method="GET" class="row g-2 mb-3 align-items-center">
|
||||
<input type="hidden" name="filter" value="{{.filter}}">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control border-start-0 ps-0 bg-white" name="q" placeholder="検索..."
|
||||
value="{{.query}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select class="form-select form-select-sm bg-white" name="priority" onchange="this.form.submit()">
|
||||
<option value="">全ての重要度</option>
|
||||
<option value="high" {{if eq .priority "high" }}selected{{end}}>高</option>
|
||||
<option value="medium" {{if eq .priority "medium" }}selected{{end}}>中</option>
|
||||
<option value="low" {{if eq .priority "low" }}selected{{end}}>低</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="/assignments?filter={{.filter}}" class="btn btn-sm btn-outline-secondary w-100 bg-white">
|
||||
クリア
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card shadow-sm border-0 rounded-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0 custom-table">
|
||||
<thead class="bg-secondary-subtle">
|
||||
<tr>
|
||||
<th style="width: 50px;" class="ps-3 text-center text-dark fw-bold">状態</th>
|
||||
<th class="text-dark fw-bold">タイトル</th>
|
||||
<th style="width: 120px;" class="text-dark fw-bold">科目</th>
|
||||
<th style="width: 80px;" class="text-dark fw-bold">重要度</th>
|
||||
<th style="width: 140px;" class="text-dark fw-bold">期限</th>
|
||||
<th style="width: 120px;" class="countdown-col text-dark fw-bold">残り</th>
|
||||
<th style="width: 80px;" class="text-end pe-3 text-dark fw-bold">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .assignments}}
|
||||
<tr class="assignment-row border-bottom" data-due-ts="{{.DueDate.Unix}}"
|
||||
data-completed="{{.IsCompleted}}">
|
||||
<td class="ps-3 text-center">
|
||||
{{if .IsCompleted}}
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit"
|
||||
class="btn btn-link p-0 text-success text-decoration-none hover-dark"
|
||||
title="未完了に戻す">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit"
|
||||
class="btn btn-link p-0 text-secondary text-decoration-none hover-dark"
|
||||
title="完了にする">
|
||||
<i class="bi bi-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark text-truncate" style="max-width: 300px;">{{.Title}}</div>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary text-white border-0 fw-bold">{{.Subject}}</span></td>
|
||||
<td>
|
||||
{{if eq .Priority "high"}}
|
||||
<span class="badge bg-danger text-white border-0 fw-bold small">高</span>
|
||||
{{else if eq .Priority "medium"}}
|
||||
<span class="badge bg-warning text-dark border-0 fw-bold small">中</span>
|
||||
{{else}}
|
||||
<span class="badge bg-dark text-white border-0 fw-bold small">低</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="small fw-bold text-dark user-select-all">{{.DueDate.Format "2006/01/02 15:04"}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="countdown-col">
|
||||
{{if not .IsCompleted}}
|
||||
<span class="countdown small fw-bold font-monospace text-dark">...</span>
|
||||
{{else}}
|
||||
<span class="text-secondary small fw-bold">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="btn-group">
|
||||
<a href="/assignments/{{.ID}}/edit" class="text-primary me-3 text-decoration-none">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</a>
|
||||
<form action="/assignments/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('削除しますか?');">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit"
|
||||
class="btn btn-link p-0 text-danger text-decoration-none border-0 bg-transparent">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-secondary fw-bold small">
|
||||
課題なし
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{if gt .totalPages 1}}
|
||||
<div class="card-footer bg-white border-top-0 py-2">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
<li class="page-item {{if not .hasPrev}}disabled{{end}}">
|
||||
<a class="page-link border-0 text-secondary"
|
||||
href="/assignments?page={{.prevPage}}&filter={{.filter}}&q={{.query}}&priority={{.priority}}">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link border-0 text-dark fw-bold">{{.currentPage}} / {{.totalPages}}</span>
|
||||
</li>
|
||||
<li class="page-item {{if not .hasNext}}disabled{{end}}">
|
||||
<a class="page-link border-0 text-secondary"
|
||||
href="/assignments?page={{.nextPage}}&filter={{.filter}}&q={{.query}}&priority={{.priority}}">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateCountdowns() {
|
||||
const now = new Date();
|
||||
document.querySelectorAll('.assignment-row').forEach(row => {
|
||||
if (row.getAttribute('data-completed') === 'true') return;
|
||||
|
||||
const dueTs = row.getAttribute('data-due-ts');
|
||||
if (!dueTs) return;
|
||||
|
||||
// Fix: Use timestamp directly to avoid parsing issues
|
||||
const due = new Date(parseInt(dueTs) * 1000);
|
||||
if (isNaN(due.getTime())) return;
|
||||
|
||||
const diff = due - now;
|
||||
const countdownEl = row.querySelector('.countdown');
|
||||
|
||||
// Reset classes
|
||||
row.classList.remove('anxiety-danger', 'anxiety-warning', 'bg-danger-subtle');
|
||||
if (countdownEl) countdownEl.className = 'countdown small fw-bold font-monospace';
|
||||
|
||||
if (diff < 0) {
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = "期限切れ";
|
||||
countdownEl.classList.add('text-danger');
|
||||
}
|
||||
row.classList.add('bg-danger-subtle');
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
|
||||
let text = "";
|
||||
let remainingHours = (days * 24) + hours;
|
||||
|
||||
if (days > 0) {
|
||||
text += `${days}日 `;
|
||||
}
|
||||
text += `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
|
||||
if (countdownEl) countdownEl.textContent = text;
|
||||
|
||||
// Anxiety Logic
|
||||
if (remainingHours < 24) {
|
||||
row.classList.add('anxiety-danger');
|
||||
if (countdownEl) {
|
||||
countdownEl.classList.add('text-danger', 'countdown-urgent');
|
||||
countdownEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill me-1"></i>' + text;
|
||||
}
|
||||
} else if (days < 7) {
|
||||
row.classList.add('anxiety-warning');
|
||||
if (countdownEl) {
|
||||
countdownEl.classList.add('text-dark');
|
||||
countdownEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i>' + text;
|
||||
}
|
||||
} else {
|
||||
if (countdownEl) countdownEl.classList.add('text-secondary');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCountdown() {
|
||||
const cols = document.querySelectorAll('.countdown-col');
|
||||
const btnText = document.getElementById('countdownBtnText');
|
||||
const isHidden = cols[0] && cols[0].style.display === 'none';
|
||||
|
||||
cols.forEach(col => {
|
||||
col.style.display = isHidden ? '' : 'none';
|
||||
});
|
||||
|
||||
btnText.textContent = isHidden ? 'カウントダウン表示中' : 'カウントダウン非表示中';
|
||||
localStorage.setItem('countdownHidden', !isHidden);
|
||||
}
|
||||
|
||||
// Init with higher frequency for smooth panic
|
||||
setInterval(updateCountdowns, 1000);
|
||||
updateCountdowns();
|
||||
|
||||
// Check preference
|
||||
const isHidden = localStorage.getItem('countdownHidden') === 'true';
|
||||
if (isHidden) {
|
||||
document.querySelectorAll('.countdown-col').forEach(col => {
|
||||
col.style.display = 'none';
|
||||
});
|
||||
const btnText = document.getElementById('countdownBtnText');
|
||||
if (btnText) btnText.textContent = 'カウントダウン非表示中';
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
51
web/templates/assignments/new.html
Normal file
51
web/templates/assignments/new.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>課題登録</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
<form method="POST" action="/assignments">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.formTitle}}" required
|
||||
autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">科目</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject" value="{{.subject}}"
|
||||
placeholder="例: 数学、英語、情報">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="priority" class="form-label">重要度</label>
|
||||
<select class="form-select" id="priority" name="priority">
|
||||
<option value="low" {{if eq .priority "low" }}selected{{end}}>小</option>
|
||||
<option value="medium" {{if not (or (eq .priority "low" ) (eq .priority "high"
|
||||
))}}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .priority "high" }}selected{{end}}>大</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">説明</label>
|
||||
<textarea class="form-control" id="description" name="description"
|
||||
rows="3">{{.description}}</textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>登録</button>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">キャンセル</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
43
web/templates/auth/login.html
Normal file
43
web/templates/auth/login.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<i class="bi bi-journal-check display-4 text-primary"></i>
|
||||
<h2 class="mt-2">ログイン</h2>
|
||||
</div>
|
||||
|
||||
{{if .error}}
|
||||
<div class="alert alert-danger">{{.error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/login">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">メールアドレス</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{.email}}" required
|
||||
autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">パスワード</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">ログイン</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-0">アカウントをお持ちでない方は</p>
|
||||
<a href="/register" class="btn btn-outline-secondary mt-2">新規登録</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
54
web/templates/auth/register.html
Normal file
54
web/templates/auth/register.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<i class="bi bi-person-plus display-4 text-primary"></i>
|
||||
<h2 class="mt-2">新規登録</h2>
|
||||
</div>
|
||||
|
||||
{{if .error}}
|
||||
<div class="alert alert-danger">{{.error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/register">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">名前</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{.name}}" required
|
||||
autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">メールアドレス</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{.email}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">パスワード</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required
|
||||
minlength="6">
|
||||
<div class="form-text">6文字以上</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password_confirm" class="form-label">パスワード(確認)</label>
|
||||
<input type="password" class="form-control" id="password_confirm" name="password_confirm"
|
||||
required minlength="6">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">登録</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-0">既にアカウントをお持ちの方は</p>
|
||||
<a href="/login" class="btn btn-outline-secondary mt-2">ログイン</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
105
web/templates/layouts/base.html
Normal file
105
web/templates/layouts/base.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.title}} - Super Homework Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
<style>
|
||||
.navbar-dark .navbar-nav .nav-link,
|
||||
.navbar-brand {
|
||||
color: #fff !important;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link:hover,
|
||||
.navbar-brand:hover {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
</style>
|
||||
{{template "head" .}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{if .userName}}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="bi bi-journal-check me-2"></i>Super Homework Manager
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="bi bi-house-door me-1"></i>ダッシュボード</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/assignments"><i class="bi bi-list-task me-1"></i>課題一覧</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/assignments/new"><i class="bi bi-plus-circle me-1"></i>課題登録</a>
|
||||
</li>
|
||||
{{if .isAdmin}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/users"><i class="bi bi-people me-1"></i>ユーザー管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/api-keys"><i class="bi bi-key me-1"></i>APIキー管理</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-1"></i>{{.userName}}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/profile"><i class="bi bi-person me-2"></i>プロフィール</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<form action="/logout" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{.csrfToken}}">
|
||||
<button type="submit" class="dropdown-item"><i
|
||||
class="bi bi-box-arrow-right me-2"></i>ログアウト</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
<main class="container py-2">
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto py-1 bg-light">
|
||||
<div class="container text-center">
|
||||
<span class="text-muted small" style="font-size: 0.75rem;">Super Homework Manager</span><br>
|
||||
<small class="text-muted" style="font-size: 0.65rem;">Licensed under <a
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPLv3</a> | Time:
|
||||
{{.processing_time}}</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
{{template "scripts" .}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "head"}}{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
300
web/templates/pages/dashboard.html
Normal file
300
web/templates/pages/dashboard.html
Normal file
@@ -0,0 +1,300 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "head"}}
|
||||
<style>
|
||||
@keyframes pulse-bg {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #ffe69c;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-bg-danger {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #f5c2c7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-banner {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.anxiety-warning {
|
||||
animation: pulse-bg 2s infinite;
|
||||
}
|
||||
|
||||
.anxiety-danger {
|
||||
animation: pulse-bg-danger 1s infinite;
|
||||
}
|
||||
|
||||
.urgent-banner {
|
||||
z-index: 1030;
|
||||
animation: blink-banner 1s infinite;
|
||||
}
|
||||
|
||||
.urgent-banner-danger {
|
||||
background: linear-gradient(90deg, #dc3545, #c82333);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.urgent-banner-warning {
|
||||
background: linear-gradient(90deg, #fd7e14, #e06c00);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.urgent-countdown {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div id="urgentBanner" class="urgent-banner py-3 text-center d-none">
|
||||
<div class="container">
|
||||
<i class="bi bi-exclamation-octagon-fill me-2"></i>
|
||||
<span id="urgentMessage"></span>
|
||||
<div class="urgent-countdown mt-1">
|
||||
<i class="bi bi-stopwatch"></i> あと <span id="urgentCountdown"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4"><i class="bi bi-house-door me-2"></i>ダッシュボード</h1>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card bg-primary text-white h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-white-50">未完了の課題</h6>
|
||||
<h2 class="mb-0">{{.stats.TotalPending}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-list-task display-4 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card bg-warning text-dark h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-dark-50">今日が期限</h6>
|
||||
<h2 class="mb-0">{{.stats.DueToday}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-calendar-event display-4 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card bg-info text-white h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-white-50">今週が期限</h6>
|
||||
<h2 class="mb-0">{{.stats.DueThisWeek}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-calendar-week display-4 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card bg-danger text-white h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-white-50">期限切れ</h6>
|
||||
<h2 class="mb-0">{{.stats.Overdue}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-exclamation-triangle display-4 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{{if .overdue}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white"><i class="bi bi-exclamation-triangle me-2"></i>期限切れの課題</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .overdue}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<div>
|
||||
<strong>{{.Title}}</strong>
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger ms-1">重要</span>{{end}}
|
||||
{{if .Subject}}<span class="badge bg-secondary ms-2">{{.Subject}}</span>{{end}}
|
||||
<br><small class="text-danger">{{formatDateTime .DueDate}}</small>
|
||||
</div>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST"><input type="hidden" name="_csrf"
|
||||
value="{{$.csrfToken}}"><button type="submit" class="btn btn-sm btn-success"
|
||||
title="完了にする"><i class="bi bi-check-lg"></i></button></form>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .dueToday}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-dark"><i class="bi bi-calendar-event me-2"></i>今日が期限</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .dueToday}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<div>
|
||||
<strong>{{.Title}}</strong>
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger ms-1">重要</span>{{end}}
|
||||
{{if .Subject}}<span class="badge bg-secondary ms-2">{{.Subject}}</span>{{end}}
|
||||
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
||||
</div>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST"><input type="hidden" name="_csrf"
|
||||
value="{{$.csrfToken}}"><button type="submit" class="btn btn-sm btn-success"
|
||||
title="完了にする"><i class="bi bi-check-lg"></i></button></form>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .upcoming}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-info">
|
||||
<div class="card-header bg-info text-white"><i class="bi bi-calendar-week me-2"></i>今週の課題</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .upcoming}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<div>
|
||||
<strong>{{.Title}}</strong>
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger ms-1">重要</span>{{end}}
|
||||
{{if .Subject}}<span class="badge bg-secondary ms-2">{{.Subject}}</span>{{end}}
|
||||
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
||||
</div>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST"><input type="hidden" name="_csrf"
|
||||
value="{{$.csrfToken}}"><button type="submit" class="btn btn-sm btn-success"
|
||||
title="完了にする"><i class="bi bi-check-lg"></i></button></form>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if and (not .overdue) (not .dueToday) (not .upcoming)}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-emoji-smile display-1 text-success"></i>
|
||||
<h3 class="mt-3">今週の課題はありません!</h3>
|
||||
<p class="text-muted">新しい課題を登録しましょう</p>
|
||||
<a href="/assignments/new" class="btn btn-primary"><i class="bi bi-plus-circle me-1"></i>課題を登録</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
(function () {
|
||||
var banner = document.getElementById('urgentBanner');
|
||||
var message = document.getElementById('urgentMessage');
|
||||
var countdown = document.getElementById('urgentCountdown');
|
||||
var body = document.body;
|
||||
|
||||
var items = document.querySelectorAll('[data-priority="high"][data-due]');
|
||||
var mostUrgent = null;
|
||||
var mostUrgentDue = Infinity;
|
||||
|
||||
items.forEach(function (item) {
|
||||
var due = parseInt(item.dataset.due);
|
||||
var now = Date.now();
|
||||
var diff = due - now;
|
||||
|
||||
if (diff > 0 && diff < mostUrgentDue) {
|
||||
mostUrgentDue = diff;
|
||||
var titleEl = item.querySelector('strong');
|
||||
mostUrgent = { due: due, title: titleEl ? titleEl.textContent : '課題' };
|
||||
}
|
||||
});
|
||||
|
||||
var hasOverdueHigh = false;
|
||||
var overdueItems = document.querySelectorAll('[data-priority="high"]');
|
||||
overdueItems.forEach(function (item) {
|
||||
var due = parseInt(item.dataset.due);
|
||||
if (due && due < Date.now()) {
|
||||
hasOverdueHigh = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasOverdueHigh) {
|
||||
banner.classList.remove('d-none');
|
||||
banner.classList.add('urgent-banner-danger');
|
||||
message.innerHTML = '🚨 <strong>期限切れの重要課題があります!</strong>';
|
||||
countdown.textContent = '今すぐ対応してください!';
|
||||
body.classList.add('anxiety-danger');
|
||||
} else if (mostUrgent && mostUrgentDue < 24 * 60 * 60 * 1000) {
|
||||
banner.classList.remove('d-none');
|
||||
banner.classList.add('urgent-banner-danger');
|
||||
message.innerHTML = '🚨 <strong>「' + mostUrgent.title + '」の期限が迫っています!</strong>';
|
||||
body.classList.add('anxiety-danger');
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 1000);
|
||||
} else if (mostUrgent && mostUrgentDue < 3 * 24 * 60 * 60 * 1000) {
|
||||
banner.classList.remove('d-none');
|
||||
banner.classList.add('urgent-banner-warning');
|
||||
message.innerHTML = '⚠️ <strong>「' + mostUrgent.title + '」の期限が近づいています</strong>';
|
||||
body.classList.add('anxiety-warning');
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
if (!mostUrgent) return;
|
||||
var now = Date.now();
|
||||
var diff = mostUrgent.due - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
countdown.textContent = '期限切れ!';
|
||||
return;
|
||||
}
|
||||
|
||||
var days = Math.floor(diff / 86400000);
|
||||
var hours = Math.floor((diff % 86400000) / 3600000);
|
||||
var mins = Math.floor((diff % 3600000) / 60000);
|
||||
var secs = Math.floor((diff % 60000) / 1000);
|
||||
|
||||
var text = '';
|
||||
if (days > 0) text = days + '日 ' + hours + '時間 ' + mins + '分 ' + secs + '秒';
|
||||
else if (hours > 0) text = hours + '時間 ' + mins + '分 ' + secs + '秒';
|
||||
else text = mins + '分 ' + secs + '秒';
|
||||
|
||||
countdown.textContent = text;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
10
web/templates/pages/error.html
Normal file
10
web/templates/pages/error.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-exclamation-triangle display-1 text-danger"></i>
|
||||
<h1 class="mt-4">{{.title}}</h1>
|
||||
<p class="lead text-muted">{{.message}}</p>
|
||||
<a href="/" class="btn btn-primary mt-3"><i class="bi bi-house-door me-1"></i>ダッシュボードに戻る</a>
|
||||
</div>
|
||||
{{end}}
|
||||
71
web/templates/pages/profile.html
Normal file
71
web/templates/pages/profile.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="mb-4"><i class="bi bi-person me-2"></i>プロフィール</h1>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">アカウント情報</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .success}}<div class="alert alert-success">{{.success}}</div>{{end}}
|
||||
<form method="POST" action="/profile">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">メールアドレス</label>
|
||||
<input type="email" class="form-control" id="email" value="{{.user.Email}}" disabled>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">名前</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{.user.Name}}"
|
||||
required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ロール</label>
|
||||
<input type="text" class="form-control"
|
||||
value="{{if eq .user.Role `admin`}}管理者{{else}}ユーザー{{end}}" disabled>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>更新</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">パスワード変更</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .passwordError}}<div class="alert alert-danger">{{.passwordError}}</div>{{end}}
|
||||
{{if .passwordSuccess}}<div class="alert alert-success">{{.passwordSuccess}}</div>{{end}}
|
||||
<form method="POST" action="/profile/password">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="old_password" class="form-label">現在のパスワード</label>
|
||||
<input type="password" class="form-control" id="old_password" name="old_password"
|
||||
required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">新しいパスワード</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password"
|
||||
required minlength="6">
|
||||
<div class="form-text">6文字以上</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">新しいパスワード(確認)</label>
|
||||
<input type="password" class="form-control" id="confirm_password"
|
||||
name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning"><i class="bi bi-key me-1"></i>パスワード変更</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user