Compare commits
18 Commits
6d5e414da2
...
develop
Author | SHA1 | Date | |
---|---|---|---|
8f92a29eef | |||
a3249a6067 | |||
d19a7119b2 | |||
3dcc69a49a | |||
e8094c6e75 | |||
5c29ad9cf6 | |||
8949e4db12 | |||
d22c39c333 | |||
1b030717b6 | |||
9ed5b7f921 | |||
a68c41226c | |||
bd8c2ef5bf | |||
b5a9ead912 | |||
a9872e2208 | |||
7de089cf6e | |||
2acdef30a7 | |||
c53f850806 | |||
8b23051fde |
@@ -6,8 +6,7 @@
|
|||||||
- Python
|
- Python
|
||||||
- Flask
|
- Flask
|
||||||
- JavaScript
|
- JavaScript
|
||||||
- Bootstrap or Tailwind テンプレート(未定)
|
- MySQL/MariaDB
|
||||||
- MySQL
|
|
||||||
|
|
||||||
## 注意事項
|
## 注意事項
|
||||||
本Webアプリは現在開発中であり、動作しない。
|
本Webアプリは現在開発中であり、動作しない。
|
@@ -3,7 +3,7 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
#login_manager.login_view = 'auth.login'
|
#login_manager.login_view = 'auth.login'
|
||||||
login_manager.login_message_category = 'info'
|
login_manager.login_message_category = 'info'
|
||||||
@@ -16,10 +16,11 @@ def create_app():
|
|||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:password@localhost/workout'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:password@localhost/workout'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
#db.init_app(app)
|
db.init_app(app)
|
||||||
#login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
|
|
||||||
#with app.app_context():
|
with app.app_context():
|
||||||
#db.create_all()
|
from . import models
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
return app
|
return app
|
280
app/assets/add-workout.html
Normal file
280
app/assets/add-workout.html
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<!-- プロトタイプ -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta content="width-device-width, initial-scale=1.0" name="viewport" />
|
||||||
|
<title>WB-Workout - ワークアウト追加</title>
|
||||||
|
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
||||||
|
<link crossorigin="" href="https://fonts.gstatic.com/" rel="preconnect" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Lexend:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="data:image/x-icon;base64," rel="icon" type="image/x-icon" />
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
:root {
|
||||||
|
--primary-color: #2a8fed;
|
||||||
|
--primary-hover-color: #1e73db;
|
||||||
|
--text-color: #111827;
|
||||||
|
--text-muted-color: #6b7280;
|
||||||
|
--background-color: #f9fafb;
|
||||||
|
--card-background-color: #ffffff;
|
||||||
|
--border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lexend', sans-serif;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
@apply bg-[var(--primary-color)] text-white font-semibold py-2.5 px-5 rounded-md shadow-sm hover:bg-[var(--primary-hover-color)] transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--primary-color)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
@apply bg-gray-200 text-[var(--text-muted-color)] font-semibold py-2 px-4 rounded-md hover:bg-gray-300 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply w-full rounded-md border-[var(--border-color)] bg-[var(--card-background-color)] text-[var(--text-color)] placeholder-[var(--text-muted-color)] focus:border-[var(--primary-color)] focus:ring-[var(--primary-color)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
@apply text-[var(--primary-color)] font-semibold border-b-2 border-[var(--primary-color)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu {
|
||||||
|
@apply hidden md:hidden absolute top-full left-0 w-full bg-[var(--card-background-color)] shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu.active {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="relative flex size-full min-h-screen flex-col">
|
||||||
|
<header class="sticky top-0 z-20 flex items-center justify-between whitespace-nowrap border-b border-solid border-[var(--border-color)] bg-[var(--card-background-color)] px-4 py-4 shadow-sm sm:px-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a class="flex items-center gap-2 text-xl font-bold text-[var(--primary-color)] md:text-2xl" href="#">
|
||||||
|
<span>WB-Workout</span>
|
||||||
|
</a>
|
||||||
|
<nav class="ml-8 hidden items-center gap-6 md:flex">
|
||||||
|
<!--
|
||||||
|
<a class="text-sm font-medium text-[var(--text-muted-color)] transition-colors duration-200 hover:text-[var(--primary-color)]" href="#">ダッシュボード</a>
|
||||||
|
<a class="nav-link active pb-1 text-sm font-medium transition-colors duration-200" href="#">ワークアウト</a>
|
||||||
|
<a class="text-sm font-medium text-[var(--text-muted-color)] transition-colors duration-200 hover:text-[var(--primary-color)]" href="#">進捗</a>
|
||||||
|
<a class="text-sm font-medium text-[var(--text-muted-color)] transition-colors duration-200 hover:text-[var(--primary-color)]" href="#">コミュニティ</a>
|
||||||
|
-->
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
|
<!-- アイコン -->
|
||||||
|
<div class="aspect-square size-10 rounded-full bg-cover bg-center" style='background-image: url("");'></div>
|
||||||
|
<button class="p-2 md:hidden" id="mobile-menu-button">
|
||||||
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path d="M4 6h16M4 12h16m-7 6h7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav class="mobile-menu" id="mobile-menu">
|
||||||
|
<!--
|
||||||
|
<a class="block px-4 py-3 text-sm text-[var(--text-muted-color)] hover:bg-gray-100" href="#">ダッシュボード</a>
|
||||||
|
<a class="block bg-gray-100 px-4 py-3 text-sm font-semibold text-[var(--primary-color)]" href="#">ワークアウト</a>
|
||||||
|
<a class="block px-4 py-3 text-sm text-[var(--text-muted-color)] hover:bg-gray-100" href="#">進捗</a>
|
||||||
|
<a class="block px-4 py-3 text-sm text-[var(--text-muted-color)] hover:bg-gray-100" href="#">コミュニティ</a>
|
||||||
|
-->
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex flex-1 justify-center px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<div class="w-full max-w-2xl space-y-8">
|
||||||
|
<header>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl">ワークアウト追加</h1>
|
||||||
|
<p class="mt-1 text-[var(--text-muted-color)] sm:text-base">最新のトレーニングセッションを記録しましょう。</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="rounded-md border border-[var(--border-color)] bg-[var(--card-background-color)] p-6 shadow-sm sm:p-8">
|
||||||
|
<form class="space-y-6" id="workout-form">
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-color)]" for="date">日付</label>
|
||||||
|
<input class="form-input mt-1" id="date" name="date" type="date" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-color)]" for="exercise-select">種目</label>
|
||||||
|
<select class="form-input mt-1" id="exercise-select" name="exercise-select">
|
||||||
|
<option value="">種目を選択</option>
|
||||||
|
<option>ベンチプレス</option>
|
||||||
|
<option>スクワット</option>
|
||||||
|
<option>デッドリフト</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-color)]" for="exercise-custom">または新しい種目を入力</label>
|
||||||
|
<input class="form-input mt-1" id="exercise-custom" name="exercise-custom" placeholder="例:バイセップカール" type="text" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--text-color)]">セット</h3>
|
||||||
|
<div class="overflow-x-auto rounded-md border border-[var(--border-color)]">
|
||||||
|
<table class="min-w-full divide-y divide-[var(--border-color)]">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-[var(--text-muted-color)]" scope="col">セット</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-[var(--text-muted-color)]" scope="col">レップ</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-[var(--text-muted-color)]" scope="col">重量 (kg)</th>
|
||||||
|
<th class="relative px-4 py-3" scope="col"><span class="sr-only">削除</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-[var(--border-color)] bg-[var(--card-background-color)]" id="sets-table-body">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button class="secondary-button" id="add-set-btn" type="button">セットを追加</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--text-color)]">前回のワークアウト</h3>
|
||||||
|
<div class="rounded-md border border-dashed border-[var(--border-color)] bg-[var(--background-color)] p-4">
|
||||||
|
<p class="text-sm text-[var(--text-muted-color)]"><span class="font-medium text-[var(--text-color)]">2024-01-15</span> の記録:</p>
|
||||||
|
<div class="mt-2 flex items-center justify-between">
|
||||||
|
<p class="font-semibold text-[var(--text-color)]">3セット 8レップ 50kg</p>
|
||||||
|
<button class="text-sm font-semibold text-[var(--primary-color)] transition-all hover:underline" id="use-template-btn" type="button">テンプレートとして使用</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4">
|
||||||
|
<button class="primary-button w-full" type="submit">ワークアウトを保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="mt-auto border-t border-[var(--border-color)] py-6 text-center text-sm text-[var(--text-muted-color)]">
|
||||||
|
<p>Powered by WB-Workout</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
const dateInput = document.getElementById('date');
|
||||||
|
const workoutForm = document.getElementById('workout-form');
|
||||||
|
const setsTableBody = document.getElementById('sets-table-body');
|
||||||
|
const addSetBtn = document.getElementById('add-set-btn');
|
||||||
|
const useTemplateBtn = document.getElementById('use-template-btn');
|
||||||
|
const exerciseSelect = document.getElementById('exercise-select');
|
||||||
|
const exerciseCustom = document.getElementById('exercise-custom');
|
||||||
|
|
||||||
|
const updateSetNumbers = () => {
|
||||||
|
const setRows = setsTableBody.querySelectorAll('tr');
|
||||||
|
setRows.forEach((row, index) => {
|
||||||
|
row.querySelector('td:first-child').textContent = index + 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新しいセットの行をテーブルに追加する
|
||||||
|
* @param {string} reps - レップ数の初期値
|
||||||
|
* @param {string} weight - 重量(kg)の初期値
|
||||||
|
*/
|
||||||
|
const addSetRow = (reps = '', weight = '') => {
|
||||||
|
const newRow = document.createElement('tr');
|
||||||
|
const setNumber = setsTableBody.rows.length + 1;
|
||||||
|
|
||||||
|
newRow.innerHTML = `
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-[var(--text-color)]">${setNumber}</td>
|
||||||
|
<td class="px-4 py-2"><input class="form-input h-10 w-24 text-center" name="reps" type="number" value="${reps}" /></td>
|
||||||
|
<td class="px-4 py-2"><input class="form-input h-10 w-24 text-center" name="weight" type="number" value="${weight}" /></td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<button class="delete-set-btn text-xl text-[var(--text-muted-color)] transition-colors hover:text-red-500" type="button">×</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
setsTableBody.appendChild(newRow);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日付入力に今日の日付を設定
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(today.getDate()).padStart(2, '0');
|
||||||
|
dateInput.value = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
// 初期状態で3セット追加、この部分は将来的にカスタマイズできたらいいかも
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
addSetRow();
|
||||||
|
}
|
||||||
|
mobileMenuButton.addEventListener('click', () => {
|
||||||
|
mobileMenu.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// セット追加ボタン
|
||||||
|
addSetBtn.addEventListener('click', () => {
|
||||||
|
addSetRow();
|
||||||
|
});
|
||||||
|
|
||||||
|
// セット削除
|
||||||
|
setsTableBody.addEventListener('click', (event) => {
|
||||||
|
if (event.target.classList.contains('delete-set-btn')) {
|
||||||
|
event.target.closest('tr').remove();
|
||||||
|
updateSetNumbers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// テンプレートとして使用ボタン
|
||||||
|
useTemplateBtn.addEventListener('click', () => {
|
||||||
|
setsTableBody.innerHTML = '';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
addSetRow('8', '50');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exerciseSelect.addEventListener('change', () => {
|
||||||
|
if (exerciseSelect.value) {
|
||||||
|
exerciseCustom.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exerciseCustom.addEventListener('input', () => {
|
||||||
|
if (exerciseCustom.value) {
|
||||||
|
exerciseSelect.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
workoutForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(workoutForm);
|
||||||
|
const sets = [];
|
||||||
|
const repsInputs = formData.getAll('reps');
|
||||||
|
const weightInputs = formData.getAll('weight');
|
||||||
|
|
||||||
|
repsInputs.forEach((rep, index) => {
|
||||||
|
sets.push({
|
||||||
|
set: index + 1,
|
||||||
|
reps: rep ? parseInt(rep, 10) : 0,
|
||||||
|
weight: weightInputs[index] ? parseFloat(weightInputs[index]) : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const workoutData = {
|
||||||
|
date: formData.get('date'),
|
||||||
|
exercise: formData.get('exercise-select') || formData.get('exercise-custom') || 'N/A',
|
||||||
|
sets: sets
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('保存されたワークアウトデータ:', workoutData);
|
||||||
|
alert('ワークアウトがコンソールに保存されました!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
268
app/assets/dashboard.html
Normal file
268
app/assets/dashboard.html
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<!-- 作りかけのプロトタイプ -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>WB-Workout - ワークアウトダッシュボード</title>
|
||||||
|
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||||
|
<link crossorigin="" href="https://fonts.gstatic.com/" rel="preconnect"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Lexend:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style type="text/tailwindcss"></style>
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
:root {
|
||||||
|
--primary-color: #2a8fed;
|
||||||
|
--primary-hover-color: #1e73db;
|
||||||
|
--text-color: #111827;
|
||||||
|
--text-muted-color: #6b7280;
|
||||||
|
--background-color: #f9fafb;
|
||||||
|
--card-background-color: #ffffff;
|
||||||
|
--border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Lexend', sans-serif;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.primary-button {
|
||||||
|
@apply bg-[var(--primary-color)] text-white font-semibold py-2 px-4 rounded-md shadow-sm hover:bg-[var(--primary-hover-color)] transition-colors duration-300;
|
||||||
|
}
|
||||||
|
.nav-link.active {
|
||||||
|
@apply text-[var(--primary-color)] font-semibold border-b-2 border-[var(--primary-color)];
|
||||||
|
}.mobile-menu {
|
||||||
|
@apply hidden md:hidden absolute top-full left-0 w-full bg-[var(--card-background-color)] shadow-md;
|
||||||
|
}
|
||||||
|
.mobile-menu.active {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="relative flex size-full min-h-screen flex-col group/design-root">
|
||||||
|
<div class="layout-container flex h-full grow flex-col">
|
||||||
|
<header class="flex items-center justify-between whitespace-nowrap border-b border-solid border-[var(--border-color)] bg-[var(--card-background-color)] px-4 sm:px-6 py-4 shadow-sm relative">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a class="flex items-center gap-2 text-xl md:text-2xl font-bold text-[var(--primary-color)]" href="#">
|
||||||
|
<!-- ロゴ画像をここに追加 -->
|
||||||
|
<span class="inline">WB-Workout</span>
|
||||||
|
</a>
|
||||||
|
<nav class="hidden md:flex items-center gap-6 ml-8">
|
||||||
|
<a class="nav-link text-sm font-medium text-[var(--text-muted-color)] hover:text-[var(--primary-color)] transition-colors duration-200" href="#">ホーム</a>
|
||||||
|
<a class="nav-link active text-sm font-medium transition-colors duration-200 pb-1" href="#">ダッシュボード</a>
|
||||||
|
<a class="nav-link text-sm font-medium text-[var(--text-muted-color)] hover:text-[var(--primary-color)] transition-colors duration-200" href="#">コミュニティ</a>
|
||||||
|
<a class="nav-link text-sm font-medium text-[var(--text-muted-color)] hover:text-[var(--primary-color)] transition-colors duration-200" href="#">設定</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
|
<div class="relative hidden sm:block">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<svg class="h-5 w-5 text-[var(--text-muted-color)]" fill="currentColor" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
|
||||||
|
</div>
|
||||||
|
<input class="form-input w-full max-w-xs rounded-md border-[var(--border-color)] bg-transparent pl-10 text-sm focus:border-[var(--primary-color)] focus:ring-[var(--primary-color)]" placeholder="検索..." type="search"/>
|
||||||
|
</div>
|
||||||
|
<button class="primary-button hidden sm:block">新しいワークアウト</button>
|
||||||
|
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10" style='background-image: url("{{ user.profile_image_url }}");'></div>
|
||||||
|
<!--↑ユーザプロフ画像 -->
|
||||||
|
<button class="md:hidden p-2" id="mobile-menu-button">
|
||||||
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path d="M4 6h16M4 12h16m-7 6h7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav class="mobile-menu" id="mobile-menu">
|
||||||
|
<a class="block px-4 py-2 text-sm text-[var(--text-muted-color)] hover:bg-gray-100" href="#">ホーム</a>
|
||||||
|
<a class="block px-4 py-2 text-sm text-[var(--primary-color)] font-semibold bg-gray-100" href="#">ダッシュボード</a>
|
||||||
|
<a class="block px-4 py-2 text-sm text-[var(--text-muted-color)] hover:bg-gray-100" href="#">コミュニティ</a>
|
||||||
|
<a class="block px-4 py-2 text-sm text-[var(--text-muted-color)] hover:bg-gray-100" href="#">設定</a>
|
||||||
|
<div class="border-t border-[var(--border-color)] px-4 py-3">
|
||||||
|
<button class="primary-button w-full">新しいワークアウト</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main class="px-4 sm:px-6 lg:px-8 py-8 flex flex-1 justify-center">
|
||||||
|
<div class="layout-content-container flex flex-col max-w-7xl w-full gap-6 md:gap-8">
|
||||||
|
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight">ワークアウトダッシュボード</h1>
|
||||||
|
<p class="text-[var(--text-muted-color)] mt-1 text-sm sm:text-base">あなたの進捗をグラフと表で確認しましょう。</p>
|
||||||
|
</div>
|
||||||
|
<!-- 開発中
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="text-sm font-medium px-3 py-1.5 rounded-md bg-[var(--primary-color)] text-white">日別</button>
|
||||||
|
<button class="text-sm font-medium px-3 py-1.5 rounded-md bg-gray-200 text-[var(--text-muted-color)] hover:bg-gray-300">週別</button>
|
||||||
|
<button class="text-sm font-medium px-3 py-1.5 rounded-md bg-gray-200 text-[var(--text-muted-color)] hover:bg-gray-300">月別</button>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
<section class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div class="lg:col-span-2 flex flex-col gap-6">
|
||||||
|
<div class="rounded-md border border-[var(--border-color)] p-4 sm:p-6 bg-[var(--card-background-color)] shadow-sm">
|
||||||
|
<h2 class="font-semibold text-lg mb-4">総リフト重量の推移</h2>
|
||||||
|
<div class="flex items-center gap-2 text-sm mb-4">
|
||||||
|
<p class="text-[var(--text-muted-color)]">過去7日間</p>
|
||||||
|
<!-- 過去7日間とあるが、筋トレしていない日は表示を省く -->
|
||||||
|
<span class="flex items-center text-green-600 font-semibold">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path clip-rule="evenodd" d="M10 17a.75.75 0 01-.75-.75V5.612L6.22 8.78a.75.75 0 01-1.06-1.06l4.25-4.25a.75.75 0 011.06 0l4.25 4.25a.75.75 0 11-1.06 1.06L10.75 5.612V16.25A.75.75 0 0110 17z" fill-rule="evenodd"></path></svg>
|
||||||
|
5%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-52 sm:h-64 relative">
|
||||||
|
<canvas id="totalLiftChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="rounded-md border border-[var(--border-color)] p-4 sm:p-6 bg-[var(--card-background-color)] shadow-sm">
|
||||||
|
<h2 class="font-semibold text-lg mb-4">ジム訪問回数</h2>
|
||||||
|
<div class="flex items-end gap-8">
|
||||||
|
<div>
|
||||||
|
<p class="text-4xl font-bold text-[var(--primary-color)]">{{ stats.gym_visits.monthly_count }}回</p>
|
||||||
|
<p class="text-[var(--text-muted-color)] text-sm mt-1">今月</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-4xl font-bold text-[var(--primary-color)]">{{ stats.gym_visits.yearly_count }}回</p>
|
||||||
|
<p class="text-[var(--text-muted-color)] text-sm mt-1">今年</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-4">
|
||||||
|
<h2 class="text-xl font-bold">ワークアウト履歴</h2>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto rounded-md border border-[var(--border-color)] bg-[var(--card-background-color)] shadow-sm">
|
||||||
|
<table class="w-full text-sm text-left">
|
||||||
|
<thead class="bg-gray-50 border-b border-[var(--border-color)]">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 font-semibold tracking-wider" scope="col">日付</th>
|
||||||
|
<th class="px-4 py-3 font-semibold tracking-wider" scope="col">運動の種類</th>
|
||||||
|
<th class="px-4 py-3 font-semibold tracking-wider text-center" scope="col">セット数</th>
|
||||||
|
<th class="px-4 py-3 font-semibold tracking-wider hidden md:table-cell" scope="col">レップ数</th>
|
||||||
|
<th class="px-4 py-3 font-semibold tracking-wider hidden sm:table-cell" scope="col">重量 (kg)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-[var(--border-color)]">
|
||||||
|
{% for workout in workout_history_7times %}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap font-medium">{{ workout.date.strftime('%Y-%m-%d') }}</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-[var(--text-muted-color)]">{{ workout.type }}</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-[var(--text-muted-color)] text-center">{{ workout.sets }}</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-[var(--text-muted-color)] hidden md:table-cell">{{ workout.reps }}</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-[var(--text-muted-color)] hidden sm:table-cell">{{ workout.weights_kg }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-8 text-[var(--text-muted-color)]">ワークアウト履歴がありません。</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="border-t border-[var(--border-color)] py-6 text-center text-sm text-[var(--text-muted-color)]">
|
||||||
|
<p>Powered by WB-Workout</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
mobileMenuButton.addEventListener('click', () => {
|
||||||
|
mobileMenu.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const workoutData = [
|
||||||
|
{% for workout in workout_history_7times %}
|
||||||
|
{
|
||||||
|
date: '{{ workout.date.strftime('%Y-%m-%d') }}',
|
||||||
|
weight: {{ workout.weights_kg}}
|
||||||
|
}{% if not loop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
|
||||||
|
const ctx = document.getElementById('totalLiftChart').getContext('2d');
|
||||||
|
|
||||||
|
const primaryColor = '#2a8fed';
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${primaryColor}4D`);
|
||||||
|
gradient.addColorStop(1, `${primaryColor}00`);
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: workoutData.map(d => d.date),
|
||||||
|
datasets: [{
|
||||||
|
label: '総リフト重量',
|
||||||
|
data: workoutData.map(d => d.weight),
|
||||||
|
borderColor: primaryColor,
|
||||||
|
backgroundColor: gradient,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
pointHoverBackgroundColor: primaryColor,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
grid: {
|
||||||
|
color: '#e5e7eb',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#6b7280',
|
||||||
|
maxTicksLimit: 6,
|
||||||
|
callback: function(value) {
|
||||||
|
return value.toLocaleString() + ' kg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#6b7280',
|
||||||
|
font: { size: 11, },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: '#111827',
|
||||||
|
titleFont: { size: 14, weight: 'bold' },
|
||||||
|
bodyFont: { size: 12 },
|
||||||
|
padding: 12,
|
||||||
|
cornerRadius: 6,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
title: function(context) {
|
||||||
|
return context[0].label;
|
||||||
|
},
|
||||||
|
label: function(context) {
|
||||||
|
return `合計: ${context.parsed.y.toLocaleString()} kg`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
61
app/assets/login.html
Normal file
61
app/assets/login.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<!-- 作りかけのプロトタイプ -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>WB-Workout - ログイン</title>
|
||||||
|
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||||
|
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Lexend:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
:root {
|
||||||
|
--primary-color: #2a8fed;
|
||||||
|
--primary-hover-color: #1e78d6;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Lexend', 'Noto Sans JP', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 text-gray-900">
|
||||||
|
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12">
|
||||||
|
<div class="w-full max-w-md space-y-8">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-center gap-3">
|
||||||
|
<!-- ロゴ画像をここに追加 -->
|
||||||
|
<h1 class="text-3xl font-bold tracking-tighter text-gray-900">WB-Workout</h1>
|
||||||
|
</div>
|
||||||
|
<h2 class="mt-6 text-center text-2xl font-bold tracking-tight text-gray-800">おかえりなさい!</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">アカウントにログインしてください</p>
|
||||||
|
</div>
|
||||||
|
<form action="#" class="mt-8 space-y-6" method="POST">
|
||||||
|
<div class="space-y-4 rounded-md shadow-sm">
|
||||||
|
<div>
|
||||||
|
<label class="sr-only" for="email-address">メールアドレス</label>
|
||||||
|
<input autocomplete="email" class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-3 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-[var(--primary-color)] focus:outline-none focus:ring-[var(--primary-color)] sm:text-sm" id="email-address" name="email" placeholder="メールアドレス" required="" type="email"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="sr-only" for="password">パスワード</label>
|
||||||
|
<input autocomplete="current-password" class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-3 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-[var(--primary-color)] focus:outline-none focus:ring-[var(--primary-color)] sm:text-sm" id="password" name="password" placeholder="パスワード" required="" type="password"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<div class="text-sm">
|
||||||
|
<a class="font-medium text-[var(--primary-color)] hover:text-[var(--primary-hover-color)]" href="#">パスワードをお忘れですか?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="group relative flex w-full justify-center rounded-md border border-transparent bg-[var(--primary-color)] px-4 py-3 text-sm font-semibold text-white hover:bg-[var(--primary-hover-color)] focus:outline-none focus:ring-2 focus:ring-[var(--primary-color)] focus:ring-offset-2" type="submit">
|
||||||
|
ログイン
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-6 text-center text-sm text-gray-600">
|
||||||
|
アカウントをお持ちでないですか?
|
||||||
|
<a class="font-medium text-[var(--primary-color)] hover:text-[var(--primary-hover-color)]" href="#">新規登録</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
@@ -1,2 +1 @@
|
|||||||
from . import db
|
from .workout import Workout, User, Exercise, WorkoutLog, GymAdmissionLog
|
||||||
from datetime import datetime
|
|
||||||
|
@@ -63,3 +63,12 @@ class WorkoutLog(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Log: Set {self.set_number} of {self.exercise.name}>'
|
return f'<Log: Set {self.set_number} of {self.exercise.name}>'
|
||||||
|
|
||||||
|
class GymAdmissionLog(db.model):
|
||||||
|
__tablename__ = 'gym_admission_logs'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
gym_name = db.Column(db.String(120), nullable=False)
|
||||||
|
enter_at = db.Column(db.DateTime, nullable=False)
|
||||||
|
exit_at = db.Column(db.DateTime, nullable=False)
|
@@ -1 +1 @@
|
|||||||
from app.models.workout import Workout
|
from .workout_repository import WorkoutRepository
|
@@ -1,31 +1,29 @@
|
|||||||
from app.models.workout import Workout
|
from app import db
|
||||||
|
from app.models import Workout
|
||||||
|
from typing import List
|
||||||
|
|
||||||
class WorkoutRepository:
|
class WorkoutRepository:
|
||||||
def __init__(self):
|
def add_workout(self, workout: Workout) -> Workout:
|
||||||
self.workouts = []
|
db.session.add(workout)
|
||||||
|
db.session.commit()
|
||||||
|
return workout
|
||||||
|
|
||||||
def add_workout(self, workout: Workout):
|
def get_all_workouts(self) -> List[Workout]:
|
||||||
self.workouts.append(workout)
|
return Workout.query.all()
|
||||||
|
|
||||||
def get_all_workouts(self):
|
def get_workout_by_id(self, workout_id: int) -> Workout | None:
|
||||||
return self.workouts
|
return Workout.query.get(workout_id)
|
||||||
|
|
||||||
def get_workout_by_id(self, workout_id: int):
|
def update_workout(self, workout: Workout) -> Workout:
|
||||||
for workout in self.workouts:
|
db.session.commit()
|
||||||
if workout.id == workout_id:
|
return workout
|
||||||
return workout
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update_workout(self, workout_id: int, updated_workout: Workout):
|
def delete_workout(self, workout_id: int) -> bool:
|
||||||
for i, workout in enumerate(self.workouts):
|
workout = self.get_workout_by_id(workout_id)
|
||||||
if workout.id == workout_id:
|
if workout:
|
||||||
self.workouts[i] = updated_workout
|
db.session.delete(workout)
|
||||||
return True
|
db.session.commit()
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_workout(self, workout_id: int):
|
# TODO:ジム入退時刻追加更新
|
||||||
for i, workout in enumerate(self.workouts):
|
|
||||||
if workout.id == workout_id:
|
|
||||||
del self.workouts[i]
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
@@ -1,2 +1 @@
|
|||||||
from app.repositories.workout_repository import WorkoutRepository
|
|
||||||
from app.models.workout import Workout
|
|
||||||
|
15
app/services/user_service.py
Normal file
15
app/services/user_service.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ログイン処理書きかけ
|
||||||
|
from app.models.workout import User
|
||||||
|
from app.repositories.workout_repository import WorkoutRepository
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
def __init__(self):
|
||||||
|
# self.users = []
|
||||||
|
self.workout_repository = WorkoutRepository()
|
||||||
|
|
||||||
|
def login(self, username: str, password: str) -> User:
|
||||||
|
# TODO:入力されたパスワードをハッシュ化して比較する処理を追加する
|
||||||
|
for user in self.users:
|
||||||
|
if user.username == username and user.password == password:
|
||||||
|
return user
|
||||||
|
return None
|
@@ -6,10 +6,11 @@ class WorkoutService:
|
|||||||
self.workout_repository = WorkoutRepository()
|
self.workout_repository = WorkoutRepository()
|
||||||
|
|
||||||
def get_all_workouts(self):
|
def get_all_workouts(self):
|
||||||
return self.workout_repository.get_all()
|
return self.workout_repository.get_all_workouts()
|
||||||
|
|
||||||
def get_workout_by_id(self, workout_id):
|
def get_workout_by_id(self, workout_id):
|
||||||
workout = self.workout_repository.get_by_id(workout_id)
|
workout = self.workout_repository.get_workout_by_id(workout_id)
|
||||||
if not workout:
|
if not workout:
|
||||||
return None
|
return None
|
||||||
return workout
|
return workout
|
||||||
|
# TODO:Workout追加、更新、ジム入退時刻追加更新
|
||||||
|
Reference in New Issue
Block a user