Skip to content

Instantly share code, notes, and snippets.

@ORESoftware
Created June 2, 2025 16:36
Show Gist options
  • Save ORESoftware/ebcd2687189ae8fdfbd186ae1a8b243e to your computer and use it in GitHub Desktop.
Save ORESoftware/ebcd2687189ae8fdfbd186ae1a8b243e to your computer and use it in GitHub Desktop.
kanban board
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Dashboard</title>
<style>
body {
font-family: sans-serif;
margin: 0;
background-color: #f4f7f6;
color: #333;
line-height: 1.6;
}
header {
background-color: #333;
color: white;
padding: 1em;
text-align: center;
}
.kanban-board {
display: flex;
justify-content: space-around;
padding: 20px;
gap: 20px;
overflow-x: auto; /* Allows horizontal scrolling if columns overflow */
align-items: flex-start; /* Align columns at the top */
}
.kanban-column {
background-color: #e9ecef;
border-radius: 8px;
padding: 15px;
width: 300px; /* Fixed width for columns */
flex-shrink: 0; /* Prevent columns from shrinking */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.kanban-column h2 {
text-align: center;
margin-top: 0;
margin-bottom: 15px;
color: #495057;
font-size: 1.25em;
}
.column-content {
min-height: 300px; /* To make drop zones visible */
border: 2px dashed transparent; /* For drop highlighting */
padding-bottom: 10px;
border-radius: 4px; /* Added for consistency */
}
.column-content.drag-over {
border-color: #007bff;
background-color: #e6f2ff;
}
.kanban-card {
background-color: white;
border: 1px solid #ced4da;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
cursor: grab;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
}
.kanban-card.dragging {
opacity: 0.5;
transform: scale(1.03) rotate(3deg);
cursor: grabbing;
}
.card-thumbnail {
width: 100%;
max-height: 160px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 10px;
}
.card-title {
font-size: 1.1em;
font-weight: bold;
margin: 0 0 8px 0;
color: #343a40;
}
.card-author, .card-rating, .card-approvals p {
font-size: 0.9em;
color: #6c757d;
margin: 4px 0;
}
.card-approvals p {
margin-bottom: 5px; /* Spacing for approvals label */
}
.card-approvals {
margin: 8px 0;
}
.approval-avatar-container {
display: flex;
gap: 4px; /* Spacing between avatars */
}
.approval-avatar {
display: inline-flex; /* Use flex for centering */
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #adb5bd; /* Default/pending color */
color: white;
font-size: 10px;
font-weight: bold;
box-shadow: 0 0 2px rgba(0,0,0,0.2);
}
.approval-avatar.pending { background-color: #ffc107; }
.approval-avatar.approved { background-color: #28a745; }
.approval-avatar.rejected { background-color: #dc3545; }
.card-voting {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 0.9em;
}
.card-voting label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
.card-voting input[type="number"] {
width: 80px;
padding: 6px;
border: 1px solid #ced4da;
border-radius: 4px;
margin-bottom: 8px;
}
.card-voting .platform-options label {
display: inline-block; /* For horizontal layout of checkboxes */
margin-right: 10px;
font-weight: normal;
}
.card-voting input[type="checkbox"] {
margin-right: 4px;
vertical-align: middle;
}
.card-voting p {
margin: 8px 0 5px 0;
font-weight: 500;
}
.production-actions-container { /* Wrapper for production actions for a card */
margin-top: 10px;
padding: 10px;
background-color: #f0f8ff; /* Light blue background */
border: 1px solid #cce5ff;
border-radius: 4px;
}
.production-actions-container h4 {
margin-top: 0;
margin-bottom: 8px;
font-size: 1em;
color: #004085;
}
.production-actions-container button {
display: block;
width: 100%;
padding: 8px 10px;
margin-bottom: 6px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
text-align: left;
}
.production-actions-container button:hover {
background-color: #0056b3;
}
.production-actions-container button:last-child {
margin-bottom: 0;
}
</style>
</head>
<body>
<header>
<h1>Content Workflow</h1>
</header>
<main class="kanban-board">
<div class="kanban-column" id="wip" data-column-id="wip">
<h2>WIP</h2>
<div class="column-content" data-column-id="wip-content">
<div class="kanban-card" draggable="true" id="card-1">
<img src="https://via.placeholder.com/280x160/E8117F/FFFFFF?text=ContentA" alt="Content Thumbnail" class="card-thumbnail">
<h3 class="card-title">Amazing New Video</h3>
<p class="card-author">Author: Jane Doe</p>
<div class="card-approvals">
<p>Approvals:</p>
<div class="approval-avatar-container">
<span class="approval-avatar pending" title="User1 (Pending)">U1</span>
<span class="approval-avatar approved" title="User2 (Approved)">U2</span>
</div>
</div>
<p class="card-rating">Rating: 7/10</p>
</div>
</div>
</div>
<div class="kanban-column" id="staging" data-column-id="staging">
<h2>Staging</h2>
<div class="column-content" data-column-id="staging-content">
<div class="kanban-card" draggable="true" id="card-2">
<img src="https://via.placeholder.com/280x160/367588/FFFFFF?text=ContentB" alt="Content Thumbnail" class="card-thumbnail">
<h3 class="card-title">Blog Post Draft</h3>
<p class="card-author">Author: John Smith</p>
<div class="card-approvals">
<p>Approvals:</p>
<div class="approval-avatar-container">
<span class="approval-avatar approved" title="User1 (Approved)">U1</span>
<span class="approval-avatar approved" title="User2 (Approved)">U2</span>
<span class="approval-avatar rejected" title="User3 (Rejected)">U3</span>
</div>
</div>
<p class="card-rating">Rating: 9/10</p>
<div class="card-voting">
<label for="ad-spend-card-2">Ad Spend ($):</label>
<input type="number" id="ad-spend-card-2" name="ad-spend" value="100" min="0">
<p>Publish to:</p>
<div class="platform-options">
<label><input type="checkbox" name="platform" value="fb"> FB</label>
<label><input type="checkbox" name="platform" value="ig" checked> IG</label>
<label><input type="checkbox" name="platform" value="tiktok"> TikTok</label>
<label><input type="checkbox" name="platform" value="linkedin"> LinkedIn</label>
</div>
</div>
</div>
<div class="kanban-card" draggable="true" id="card-3">
<img src="https://via.placeholder.com/280x160/FFC107/000000?text=ContentC" alt="Content Thumbnail" class="card-thumbnail">
<h3 class="card-title">Podcast Episode Idea</h3>
<p class="card-author">Author: Alex Green</p>
<div class="card-approvals">
<p>Approvals:</p>
<div class="approval-avatar-container">
<span class="approval-avatar pending" title="ReviewerA (Pending)">RA</span>
<span class="approval-avatar pending" title="ReviewerB (Pending)">RB</span>
</div>
</div>
<p class="card-rating">Rating: (Not Rated)</p>
<div class="card-voting">
<label for="ad-spend-card-3">Ad Spend ($):</label>
<input type="number" id="ad-spend-card-3" name="ad-spend" value="50" min="0">
<p>Publish to:</p>
<div class="platform-options">
<label><input type="checkbox" name="platform" value="fb"> FB</label>
<label><input type="checkbox" name="platform" value="x"> X</label>
<label><input type="checkbox" name="platform" value="spotify"> Spotify</label>
</div>
</div>
</div>
</div>
</div>
<div class="kanban-column" id="production" data-column-id="production">
<h2>Production</h2>
<div class="column-content" data-column-id="production-content">
</div>
</div>
<div class="kanban-column" id="outtake" data-column-id="outtake">
<h2>Outtake</h2>
<div class="column-content" data-column-id="outtake-content">
</div>
</div>
</main>
<script>
document.addEventListener('DOMContentLoaded', () => {
const cards = document.querySelectorAll('.kanban-card');
const columns = document.querySelectorAll('.column-content');
let draggedCard = null;
let originalColumn = null;
// --- Sample Data (In a real app, this would come from a server/API) ---
// This data would be more deeply integrated to generate cards dynamically
// and persist changes. For now, it's mainly for the production actions.
const contentDataStore = {
"card-1": {
id: "card-1",
title: "Amazing New Video",
currentColumn: "wip",
adSpend: 0,
platforms: [] // Will be populated by checkboxes if this card moves to staging
},
"card-2": {
id: "card-2",
title: "Blog Post Draft",
currentColumn: "staging",
adSpend: 100, // From input
platforms: ["ig"] // From checkboxes
},
"card-3": {
id: "card-3",
title: "Podcast Episode Idea",
currentColumn: "staging",
adSpend: 50,
platforms: []
}
};
// Initialize or update data based on existing card inputs
cards.forEach(card => {
const cardId = card.id;
if (!contentDataStore[cardId]) contentDataStore[cardId] = { id: cardId, title: card.querySelector('.card-title')?.textContent || 'Untitled', platforms: [], adSpend: 0 };
const adSpendInput = card.querySelector('input[type="number"][name="ad-spend"]');
if (adSpendInput) {
contentDataStore[cardId].adSpend = parseInt(adSpendInput.value, 10);
adSpendInput.addEventListener('change', (e) => {
contentDataStore[cardId].adSpend = parseInt(e.target.value, 10);
console.log(`Ad spend for ${cardId} updated to: ${contentDataStore[cardId].adSpend}`);
});
}
const platformCheckboxes = card.querySelectorAll('input[type="checkbox"][name="platform"]');
platformCheckboxes.forEach(checkbox => {
if (checkbox.checked) {
if (!contentDataStore[cardId].platforms.includes(checkbox.value)) {
contentDataStore[cardId].platforms.push(checkbox.value);
}
}
checkbox.addEventListener('change', (e) => {
if (e.target.checked) {
if (!contentDataStore[cardId].platforms.includes(e.target.value)) {
contentDataStore[cardId].platforms.push(e.target.value);
}
} else {
contentDataStore[cardId].platforms = contentDataStore[cardId].platforms.filter(p => p !== e.target.value);
}
console.log(`Platforms for ${cardId} updated to:`, contentDataStore[cardId].platforms);
});
});
});
// --- Drag and Drop Functionality ---
cards.forEach(card => {
card.addEventListener('dragstart', (e) => {
draggedCard = card;
originalColumn = card.parentElement; // Store the original column
setTimeout(() => card.classList.add('dragging'), 0);
// e.dataTransfer.setData('text/plain', card.id); // Not strictly needed for this same-page example if using draggedCard variable
});
card.addEventListener('dragend', () => {
if (draggedCard) { // Check if it's still the card being dragged
draggedCard.classList.remove('dragging');
}
draggedCard = null;
originalColumn = null; // Reset original column
});
});
columns.forEach(column => {
column.addEventListener('dragover', (e) => {
e.preventDefault(); // Necessary to allow dropping
if (column !== originalColumn) { // Don't highlight if over original column unless empty
column.classList.add('drag-over');
}
});
column.addEventListener('dragleave', () => {
column.classList.remove('drag-over');
});
column.addEventListener('drop', (e) => {
e.preventDefault();
column.classList.remove('drag-over');
if (draggedCard) {
const targetColumnElement = column.closest('.kanban-column');
const targetColumnId = targetColumnElement.id;
const cardId = draggedCard.id;
// Append card to the new column
column.appendChild(draggedCard);
console.log(`Card ${cardId} moved to ${targetColumnId}`);
// Update card's column in our data store
if (contentDataStore[cardId]) {
contentDataStore[cardId].currentColumn = targetColumnId;
}
// Manage Production Actions
removeProductionActions(draggedCard); // Remove from old card if it existed
if (targetColumnId === 'production') {
addProductionActions(draggedCard);
}
}
});
});
// --- Production Actions Management ---
function addProductionActions(cardElement) {
const cardId = cardElement.id;
const cardData = contentDataStore[cardId];
if (!cardData) return;
let actionsContainer = cardElement.querySelector('.production-actions-container');
if (!actionsContainer) {
actionsContainer = document.createElement('div');
actionsContainer.className = 'production-actions-container';
cardElement.appendChild(actionsContainer); // Append to the card itself
}
actionsContainer.innerHTML = ''; // Clear previous content
const title = document.createElement('h4');
title.textContent = `Publish: ${cardData.title}`;
actionsContainer.appendChild(title);
const platformsToPublish = cardData.platforms.length > 0 ? cardData.platforms : ['fb', 'ig', 'tiktok', 'linkedin']; // Default if none explicitly selected
platformsToPublish.forEach(platform => {
const btn = document.createElement('button');
btn.dataset.platform = platform;
// Simple icon mapping for demonstration
let icon = '';
if (platform.toLowerCase() === 'fb') icon = 'πŸ‘ ';
else if (platform.toLowerCase() === 'ig') icon = 'πŸ“Έ ';
else if (platform.toLowerCase() === 'tiktok') icon = '🎡 ';
else if (platform.toLowerCase() === 'linkedin') icon = 'πŸ”— ';
else if (platform.toLowerCase() === 'x') icon = '🐦 ';
else if (platform.toLowerCase() === 'spotify') icon = '🎧 ';
btn.textContent = `${icon}Connect & Publish to ${platform.toUpperCase()}`;
btn.onclick = () => alert(`Initiate publishing to ${platform.toUpperCase()} for "${cardData.title}". Ad Spend: $${cardData.adSpend || 0}`);
actionsContainer.appendChild(btn);
});
actionsContainer.style.display = 'block';
}
function removeProductionActions(cardElement) {
const actionsContainer = cardElement.querySelector('.production-actions-container');
if (actionsContainer) {
actionsContainer.remove();
}
}
// Initial check for cards already in production (e.g. if page reloaded with state)
document.querySelectorAll('.kanban-column#production .kanban-card').forEach(cardInProduction => {
addProductionActions(cardInProduction);
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment