Initial commit: MVP+ wish board with FastAPI + Tailwind
This commit is contained in:
commit
cd945a6128
2277 files changed
+843095
No files matched your search
@@ -0,0 +1,761 @@
|
||||
<!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>
|
||||
Reference in new issue
Block a user