Files
wish-board/static/index.html
T

762 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Доска желаний</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: white;
font-size: 3rem;
text-align: center;
margin-bottom: 2rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.board {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 1.5rem;
padding: 2rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.board-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
}
.board-title {
display: flex;
align-items: center;
gap: 0.75rem;
}
.board-color {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
}
.board-name {
font-size: 1.75rem;
font-weight: bold;
color: white;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: white;
color: #1f2937;
}
.btn-primary:hover {
background: #f3f4f6;
transform: translateY(-2px);
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-small {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.lists-container {
display: flex;
gap: 1.5rem;
overflow-x: auto;
padding-bottom: 1rem;
min-height: 400px;
}
.list {
background: white;
border-radius: 1rem;
padding: 1.25rem;
min-width: 300px;
max-width: 320px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
flex-shrink: 0;
}
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.list-title {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
cursor: pointer;
flex: 1;
}
.list-title:hover {
color: #4b5563;
}
.list-title-input {
flex: 1;
padding: 0.5rem;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
}
.list-title-input:focus {
outline: none;
border-color: #6366f1;
}
.cards-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-height: 100px;
}
.card {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 1rem;
cursor: grab;
transition: all 0.2s;
}
.card:hover {
background: #f3f4f6;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.card:active {
cursor: grabbing;
}
.card-title {
font-size: 0.9375rem;
font-weight: 500;
color: #1f2937;
margin-bottom: 0.5rem;
}
.card-description {
font-size: 0.8125rem;
color: #6b7280;
line-height: 1.5;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.75rem;
}
.card-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
color: #9ca3af;
border-radius: 0.25rem;
}
.card-action-btn:hover {
background: #e5e7eb;
color: #4b5563;
}
.card-action-btn.delete:hover {
background: #fee2e2;
color: #dc2626;
}
.add-card-form {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
.add-card-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.add-card-input:focus {
outline: none;
border-color: #6366f1;
}
.add-list-form {
margin-top: 2rem;
}
.add-list-container {
display: flex;
gap: 0.75rem;
}
.add-list-input {
flex: 1;
padding: 1rem;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 1rem;
background: rgba(255,255,255,0.2);
color: white;
font-size: 1rem;
}
.add-list-input::placeholder {
color: rgba(255,255,255,0.7);
}
.add-list-input:focus {
outline: none;
border-color: white;
background: rgba(255,255,255,0.3);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: white;
}
.empty-state h2 {
font-size: 2rem;
margin-bottom: 1rem;
}
.create-board-form {
background: white;
border-radius: 1.5rem;
padding: 3rem;
max-width: 500px;
margin: 0 auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.create-board-form h2 {
font-size: 1.75rem;
color: #1f2937;
margin-bottom: 2rem;
text-align: center;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.5rem;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.75rem;
font-size: 1rem;
}
.form-input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.color-picker {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
}
.color-option {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
border: 3px solid transparent;
cursor: pointer;
transition: all 0.2s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
border-color: #1f2937;
transform: scale(1.15);
}
.btn-submit {
width: 100%;
padding: 1rem;
background: #6366f1;
color: white;
border: none;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-submit:hover {
background: #4f46e5;
transform: translateY(-2px);
}
.edit-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.edit-input {
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
}
.edit-textarea {
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
resize: vertical;
min-height: 60px;
}
.edit-actions {
display: flex;
gap: 0.5rem;
}
.btn-save {
background: #10b981;
color: white;
}
.btn-save:hover {
background: #059669;
}
.btn-cancel {
background: #6b7280;
color: white;
}
.btn-cancel:hover {
background: #4b5563;
}
.dragging {
opacity: 0.5;
transform: rotate(5deg);
}
.drag-over {
background: #e0e7ff;
border: 2px dashed #6366f1;
}
</style>
</head>
<body>
<div class="container">
<h1>✨ Доска желаний</h1>
<div id="app"></div>
</div>
<script>
const API_BASE = '/api';
let currentBoard = null;
let boards = [];
async function fetchBoards() {
const res = await fetch(`${API_BASE}/boards`);
boards = await res.json();
if (boards.length > 0 && !currentBoard) {
currentBoard = boards[0];
}
render();
}
async function createBoard(name, color) {
const res = await fetch(`${API_BASE}/boards`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, color})
});
const board = await res.json();
boards.unshift(board);
currentBoard = board;
render();
}
async function deleteBoard(id) {
if (confirm('Удалить доску?')) {
await fetch(`${API_BASE}/boards/${id}`, {method: 'DELETE'});
boards = boards.filter(b => b.id !== id);
currentBoard = boards.length > 0 ? boards[0] : null;
render();
}
}
async function updateBoard(id, data) {
await fetch(`${API_BASE}/boards/${id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
fetchBoards();
}
async function fetchLists(boardId) {
const res = await fetch(`${API_BASE}/boards/${boardId}/lists`);
return await res.json();
}
async function createList(boardId, title) {
const res = await fetch(`${API_BASE}/boards/${boardId}/lists`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title})
});
return await res.json();
}
async function updateList(listId, data) {
await fetch(`${API_BASE}/lists/${listId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
async function deleteList(listId) {
if (confirm('Удалить список и все карточки?')) {
await fetch(`${API_BASE}/lists/${listId}`, {method: 'DELETE'});
}
}
async function fetchCards(listId) {
const res = await fetch(`${API_BASE}/lists/${listId}/cards`);
return await res.json();
}
async function createCard(listId, title, description = '') {
const res = await fetch(`${API_BASE}/lists/${listId}/cards`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title, description})
});
return await res.json();
}
async function updateCard(cardId, data) {
await fetch(`${API_BASE}/cards/${cardId}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
async function deleteCard(cardId) {
await fetch(`${API_BASE}/cards/${cardId}`, {method: 'DELETE'});
}
async function generateRandomCards(count = 5) {
const res = await fetch(`${API_BASE}/generate-cards?count=${count}`);
const data = await res.json();
return data.cards;
}
function render() {
const app = document.getElementById('app');
if (!currentBoard) {
app.innerHTML = `
<div class="create-board-form">
<h2>Создать новую доску</h2>
<form id="createBoardForm">
<div class="form-group">
<label class="form-label">Название доски</label>
<input type="text" class="form-input" id="boardName" placeholder="Например: Цели на 2024" required>
</div>
<div class="form-group">
<label class="form-label">Цвет доски</label>
<div class="color-picker" id="colorPicker"></div>
</div>
<button type="submit" class="btn-submit">Создать доску</button>
</form>
</div>
`;
const colors = ['#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#2563eb', '#1d4ed8', '#1e3a8a'];
const colorPicker = document.getElementById('colorPicker');
colors.forEach((color, index) => {
const div = document.createElement('div');
div.className = `color-option ${index === 0 ? 'selected' : ''}`;
div.style.backgroundColor = color;
div.dataset.color = color;
colorPicker.appendChild(div);
});
let selectedColor = colors[0];
colorPicker.addEventListener('click', (e) => {
if (e.target.classList.contains('color-option')) {
document.querySelectorAll('.color-option').forEach(el => el.classList.remove('selected'));
e.target.classList.add('selected');
selectedColor = e.target.dataset.color;
}
});
document.getElementById('createBoardForm').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('boardName').value;
if (name.trim()) {
await createBoard(name.trim(), selectedColor);
}
});
return;
}
fetchLists(currentBoard.id).then(lists => {
renderBoard(lists);
});
}
async function renderBoard(lists) {
const app = document.getElementById('app');
app.innerHTML = `
<div class="board">
<div class="board-header">
<div class="board-title">
<div class="board-color" style="background-color: ${currentBoard.color}"></div>
<h2 class="board-name">${currentBoard.name}</h2>
</div>
<div class="flex gap-2">
<button class="btn btn-danger btn-small" onclick="deleteBoard('${currentBoard.id}')">Удалить доску</button>
</div>
</div>
<div class="lists-container" id="listsContainer">
${lists.map(list => renderList(list)).join('')}
</div>
<div class="add-list-form">
<div class="add-list-container">
<input type="text" class="add-list-input" id="newListInput" placeholder="Добавить список..." onkeypress="if(event.key==='Enter') addList()">
<button class="btn btn-primary" onclick="addList()">Добавить</button>
</div>
</div>
</div>
`;
// Load cards for each list
lists.forEach(list => loadCards(list.id));
}
function renderList(list) {
return `
<div class="list" data-list-id="${list.id}">
<div class="list-header">
<h3 class="list-title" ondblclick="editListTitle('${list.id}', '${list.title}')">${list.title}</h3>
<button class="btn btn-danger btn-small" onclick="deleteList('${list.id}')">×</button>
</div>
<div class="cards-container" id="cards-${list.id}">
<!-- Cards will be loaded here -->
</div>
<div class="add-card-form">
<input type="text" class="add-card-input" placeholder="Добавить карточку..." onkeypress="if(event.key==='Enter') addCard('${list.id}', this)">
</div>
</div>
`;
}
async function loadCards(listId) {
const cards = await fetchCards(listId);
const container = document.getElementById(`cards-${listId}`);
container.innerHTML = cards.map(card => renderCard(card)).join('');
}
function renderCard(card) {
return `
<div class="card" draggable="true" data-card-id="${card.id}">
<div class="card-title">${card.title}</div>
${card.description ? `<div class="card-description">${card.description}</div>` : ''}
<div class="card-actions">
<button class="card-action-btn" onclick="editCard('${card.id}')">Изменить</button>
<button class="card-action-btn delete" onclick="deleteCard('${card.id}')">Удалить</button>
</div>
</div>
`;
}
async function addCard(listId, input) {
if (input.value.trim()) {
await createCard(listId, input.value.trim());
input.value = '';
loadCards(listId);
}
}
async function addList() {
const input = document.getElementById('newListInput');
if (input.value.trim()) {
const list = await createList(currentBoard.id, input.value.trim());
const container = document.getElementById('listsContainer');
container.insertAdjacentHTML('beforeend', renderList(list));
loadCards(list.id);
input.value = '';
}
}
async function editListTitle(listId, currentTitle) {
const titleEl = document.querySelector(`[data-list-id="${listId}"] .list-title`);
titleEl.innerHTML = `<input type="text" class="list-title-input" value="${currentTitle}" onblur="saveListTitle('${listId}', this.value)" onkeypress="if(event.key==='Enter') saveListTitle('${listId}', this.value)">`;
titleEl.querySelector('input').focus();
}
async function saveListTitle(listId, newTitle) {
if (newTitle.trim()) {
await updateList(listId, {title: newTitle.trim()});
render();
}
}
async function editCard(cardId) {
const cards = await fetchCards(document.querySelector(`[data-list-id]`).dataset.listId);
const card = cards.find(c => c.id === cardId);
if (!card) return;
const cardEl = document.querySelector(`[data-card-id="${cardId}"]`);
cardEl.innerHTML = `
<div class="edit-form">
<input type="text" class="edit-input" id="edit-title-${cardId}" value="${card.title}">
<textarea class="edit-textarea" id="edit-desc-${cardId}">${card.description}</textarea>
<div class="edit-actions">
<button class="btn btn-save btn-small" onclick="saveCard('${cardId}')">Сохранить</button>
<button class="btn btn-cancel btn-small" onclick="loadCards('${cardEl.closest('.list').dataset.listId}')">Отмена</button>
</div>
</div>
`;
}
async function saveCard(cardId) {
const title = document.getElementById(`edit-title-${cardId}`).value;
const description = document.getElementById(`edit-desc-${cardId}`).value;
await updateCard(cardId, {title, description});
render();
}
// Drag and drop
let draggedCard = null;
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('card')) {
draggedCard = e.target;
e.target.classList.add('dragging');
}
});
document.addEventListener('dragend', (e) => {
if (e.target.classList.contains('card')) {
e.target.classList.remove('dragging');
}
});
document.addEventListener('dragover', (e) => {
e.preventDefault();
const list = e.target.closest('.list');
if (list) {
list.classList.add('drag-over');
}
});
document.addEventListener('dragleave', (e) => {
const list = e.target.closest('.list');
if (list) {
list.classList.remove('drag-over');
}
});
document.addEventListener('drop', async (e) => {
e.preventDefault();
const list = e.target.closest('.list');
if (list && draggedCard) {
list.classList.remove('drag-over');
const listId = list.dataset.listId;
const cardId = draggedCard.dataset.cardId;
// Update position
const cards = await fetchCards(listId);
const cardIndex = cards.findIndex(c => c.id === cardId);
await updateCard(cardId, {position: cardIndex, list_id: listId});
// Reload all lists
fetchBoards();
draggedCard = null;
}
});
// Initial load
fetchBoards();
</script>
</body>
</html>