Created
September 7, 2024 11:23
-
-
Save opencoca/b26eadc281929a1a2a0a543c6986cc18 to your computer and use it in GitHub Desktop.
Todo Deck
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link rel="stylesheet" href="https://startr.style/style.css"> | |
<div class="stack"> | |
<div class="stack--status"> | |
<i class="fa fa-remove"></i> | |
<i class="fa fa-check"></i> | |
</div> | |
<div class="stack--cards"> | |
<div class="stack--card"> | |
<img src="https://image.startr.cloud/400x300"> | |
<h3>a Todo Deck Card</h3> | |
<p>Swipe Right or click the <i class="fa fa-check"></i> button to mark something as done.</p> | |
</div> | |
<div class="stack--card"> | |
<img src="https://image.startr.cloud/400x301"> | |
<h3>Another Card</h3> | |
<p>Swipe Left or click the <i class="fa fa-remove"></i> button to push to the back of the stack</p> | |
</div> | |
<div class="stack--card"> | |
<img src="https://image.startr.cloud/400x302"> | |
<h3>Double Click to edit</h3> | |
<p>Then push to the back of the stack</p> | |
</div> | |
<div class="stack--card"> | |
<img src="https://image.startr.cloud/400x303"> | |
<h3>Click the + button</h3> | |
<p>This gives you a new card</p> | |
</div> | |
<div class="stack--card"> | |
<img src="https://image.startr.cloud/400x304"> | |
<h3>The end :D</h3> | |
<p>We hope you enjoyed this demo. Feel free to use this for your todos (at the moment it is all in browser. Refreshing will reset everything.</p> | |
</div> | |
</div> | |
<div class="stack--buttons"> | |
<button id="nope"><i class="fa fa-remove"></i></button> | |
<button id="add-card"><i class="fa fa-plus-square" style="--c:lightgrey"></i></button> | |
<button id="edit"><i class="fa fa-edit" style="--c:lightblue; --mr:-0.1em"></i></button> | |
<button id="done"><i class="fa fa-check"></i></button> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
// Select DOM elements | |
const stackContainer = document.querySelector('.stack'); | |
const stackCardsContainer = document.querySelector('.stack--cards'); | |
const stackStatus = document.querySelector('.stack--status'); | |
const allCards = document.querySelectorAll('.stack--card'); | |
const nope = document.getElementById('nope'); | |
const done = document.getElementById('done'); | |
const addCard = document.getElementById('add-card'); | |
const editButton = document.getElementById('edit'); | |
let isOverviewMode = false; | |
let isEditMode = false; | |
let currentEditingCard = null; | |
/** | |
* Initialize the stack of cards | |
* Sets z-index, transform, and opacity for each card to create a stacked effect | |
*/ | |
function initCards() { | |
const newCards = document.querySelectorAll('.stack--card:not(.removed)'); | |
newCards.forEach((card, index) => { | |
card.style.zIndex = newCards.length - index; | |
card.style.transform = `scale(${(20 - index) / 20}) translateY(-${30 * index}px)`; | |
card.style.opacity = (10 - index) / 10; | |
}); | |
stackContainer.classList.add('loaded'); | |
} | |
/** | |
* Attach event listeners to a card | |
* @param {HTMLElement} card - The card element to attach listeners to | |
*/ | |
function attachCardListeners(card) { | |
const hammertime = new Hammer(card); | |
hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL }); | |
hammertime.on('pan', handlePan); | |
hammertime.on('panend', handlePanEnd); | |
} | |
// Initialize cards and attach listeners on script load | |
initCards(); | |
allCards.forEach(attachCardListeners); | |
/** | |
* Generate a random integer | |
* @param {number} max - The maximum value (exclusive) | |
* @returns {number} A random integer between 0 and max-1 | |
*/ | |
function getRandomInt(max) { | |
return Math.floor(Math.random() * max); | |
} | |
/** | |
* Handle the pan movement | |
* @param {Object} event - The pan event object | |
*/ | |
function handlePan(event) { | |
if (isEditMode || isOverviewMode) return; | |
const card = event.target.closest('.stack--card'); | |
if (!card) return; | |
card.classList.add('moving'); | |
// Exit if there's no movement | |
if (event.deltaX === 0 && event.deltaY === 0) return; | |
// Toggle classes based on swipe direction | |
stackContainer.classList.toggle('stack_done', event.deltaX > 0); | |
stackContainer.classList.toggle('stack_nope', event.deltaX < 0); | |
// Calculate rotation based on movement | |
const xMulti = event.deltaX * 0.03; | |
const yMulti = event.deltaY / 80; | |
const rotate = xMulti * yMulti; | |
// Apply transformation | |
card.style.transform = `translate(${event.deltaX}px, ${event.deltaY}px) rotate(${rotate}deg)`; | |
// Handle vertical swipes | |
if (Math.abs(event.deltaY) > 80) { | |
if (event.deltaY < 0) { | |
// Swipe up: enter overview mode | |
enterOverviewMode(); | |
} else { | |
// Swipe down: edit mode | |
enterEditMode(card); | |
} | |
} | |
} | |
/** | |
* Handle the end of a pan gesture | |
* @param {Object} event - The panend event object | |
*/ | |
function handlePanEnd(event) { | |
if (isEditMode || isOverviewMode) return; | |
const card = event.target.closest('.stack--card'); | |
if (!card) return; | |
card.classList.remove('moving'); | |
const moveOutWidth = document.body.clientWidth; | |
const keep = Math.abs(event.deltaX) < 80 || Math.abs(event.velocityX) < 0.5; | |
if (keep) { | |
// Reset the card position if the swipe wasn't strong enough | |
card.style.transform = ''; | |
} else { | |
// Calculate the card's final position | |
const endX = Math.max(Math.abs(event.velocityX) * moveOutWidth, moveOutWidth); | |
const toX = event.deltaX > 0 ? endX : -endX; | |
const endY = Math.abs(event.velocityY) * moveOutWidth; | |
const toY = event.deltaY > 0 ? endY : -endY; | |
const xMulti = event.deltaX * 0.03; | |
const yMulti = event.deltaY / 80; | |
const rotate = xMulti * yMulti; | |
// Apply the final transformation | |
card.style.transform = `translate(${toX}px, ${toY + event.deltaY}px) rotate(${rotate}deg)`; | |
if (event.deltaX > 0) { | |
// Swipe right: mark as removed and done | |
card.classList.add('removed', 'done'); | |
} else { | |
// Swipe left: move to back of stack | |
moveCardToBack(card); | |
} | |
// Clean up classes and reinitialize | |
stackContainer.classList.remove('stack_done', 'stack_nope'); | |
initCards(); | |
} | |
} | |
/** | |
* Enter edit mode for a card | |
* @param {HTMLElement} card - The card element to edit | |
*/ | |
function enterEditMode(card) { | |
isEditMode = true; | |
currentEditingCard = card; | |
stackStatus.style.display = 'none'; | |
const title = card.querySelector('h3'); | |
const description = card.querySelector('p'); | |
title.contentEditable = true; | |
description.contentEditable = true; | |
// Preselect the title text | |
const range = document.createRange(); | |
range.selectNodeContents(title); | |
const selection = window.getSelection(); | |
selection.removeAllRanges(); | |
selection.addRange(range); | |
title.focus(); | |
// Add a click event listener to the document to handle clicking outside | |
document.addEventListener('click', handleClickOutside); | |
} | |
/** | |
* Handle clicking outside the editable card | |
* @param {Event} e - The click event | |
*/ | |
function handleClickOutside(e) { | |
if (currentEditingCard && !currentEditingCard.contains(e.target)) { | |
exitEditMode(); | |
} | |
} | |
/** | |
* Exit edit mode | |
*/ | |
function exitEditMode() { | |
if (!isEditMode) return; | |
isEditMode = false; | |
stackStatus.style.display = ''; | |
if (currentEditingCard) { | |
const title = currentEditingCard.querySelector('h3'); | |
const description = currentEditingCard.querySelector('p'); | |
title.contentEditable = false; | |
description.contentEditable = false; | |
// Reattach event listeners to the card | |
attachCardListeners(currentEditingCard); | |
currentEditingCard = null; | |
} | |
document.removeEventListener('click', handleClickOutside); | |
initCards(); | |
} | |
/** | |
* Move a card to the back of the stack | |
* @param {HTMLElement} card - The card element to move | |
*/ | |
function moveCardToBack(card) { | |
card.classList.remove('removed', 'done'); | |
card.style.transform = ''; | |
stackCardsContainer.appendChild(card); | |
initCards(); | |
} | |
/** | |
* Create a listener for the action buttons | |
* @param {boolean} isDone - Whether this is for the 'done' action | |
* @returns {Function} Event listener function | |
*/ | |
function createButtonListener(isDone) { | |
return function(event) { | |
if (isEditMode || isOverviewMode) return; | |
const cards = document.querySelectorAll('.stack--card:not(.removed)'); | |
if (!cards.length) return false; | |
const card = cards[0]; | |
const moveOutWidth = document.body.clientWidth * 1.5; | |
if (isDone) { | |
// 'Done' action: move card to the right and mark as done | |
card.style.transform = `translate(${moveOutWidth}px, -100px) rotate(-30deg)`; | |
card.classList.add('removed', 'done'); | |
} else { | |
// 'Nope' action: move card to the back of the stack | |
moveCardToBack(card); | |
} | |
initCards(); | |
event.preventDefault(); | |
}; | |
} | |
/** | |
* Generate a new card element | |
* @returns {HTMLElement} The new card element | |
*/ | |
function generateNewCard() { | |
const card = document.createElement('div'); | |
card.className = 'stack--card'; | |
card.innerHTML = ` | |
<img src="https://image.startr.cloud/40${getRandomInt(10)}x30${getRandomInt(10)}"> | |
<h3>New Card</h3> | |
<p>This is a new card</p> | |
`; | |
attachCardListeners(card); | |
return card; | |
} | |
/** | |
* Add a new card to the stack | |
*/ | |
function addNewCard() { | |
const newCard = generateNewCard(); | |
stackCardsContainer.insertBefore(newCard, stackCardsContainer.firstChild); | |
initCards(); | |
} | |
/** | |
* Enter overview mode | |
*/ | |
function enterOverviewMode() { | |
isOverviewMode = true; | |
stackStatus.style.display = 'none'; | |
stackContainer.classList.add('overview'); | |
document.querySelectorAll('.stack--card').forEach(card => { | |
card.classList.add('spread'); | |
}); | |
} | |
/** | |
* Exit overview mode | |
*/ | |
function exitOverviewMode() { | |
isOverviewMode = false; | |
stackStatus.style.display = ''; | |
stackContainer.classList.remove('overview'); | |
document.querySelectorAll('.stack--card').forEach(card => { | |
card.classList.remove('spread'); | |
}); | |
initCards(); | |
} | |
/** | |
* Handle edit button click | |
*/ | |
function handleEditClick() { | |
if (isEditMode || isOverviewMode) return; | |
const topCard = document.querySelector('.stack--card:not(.removed)'); | |
if (topCard) { | |
enterEditMode(topCard); | |
} | |
} | |
// Attach event listeners to action buttons | |
const nopeListener = createButtonListener(false); | |
const doneListener = createButtonListener(true); | |
nope.addEventListener('click', nopeListener); | |
done.addEventListener('click', doneListener); | |
addCard.addEventListener('click', addNewCard); | |
editButton.addEventListener('click', handleEditClick); | |
// Add event listener for exiting overview mode | |
document.addEventListener('keydown', (e) => { | |
if (e.key === 'Escape') { | |
if (isOverviewMode) { | |
exitOverviewMode(); | |
} else if (isEditMode) { | |
exitEditMode(); | |
} | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://hammerjs.github.io/dist/hammer.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*, *:before, *:after { | |
box-sizing: border-box; | |
padding: 0; | |
margin: 0; | |
} | |
body { | |
background: #CCFBFE; | |
overflow: hidden; | |
font-family: sans-serif; | |
} | |
.stack { | |
width: 100vw; | |
height: 100vh; | |
overflow: hidden; | |
display: flex; | |
flex-direction: column; | |
position: relative; | |
opacity: 0; | |
transition: opacity 0.1s ease-in-out; | |
} | |
.loaded.stack { | |
opacity: 1; | |
} | |
.stack--status { | |
position: absolute; | |
top: 50%; | |
z-index: 2; | |
width: 100%; | |
text-align: center; | |
pointer-events: none; | |
} | |
.stack--status i { | |
font-size: 100px; | |
opacity: 0; | |
transform: scale(0.3); | |
transition: all 0.2s ease-in-out; | |
position: absolute; | |
width: 100px; | |
margin-left: -50px; | |
} | |
.stack_done .fa-check { | |
opacity: 0.7; | |
transform: scale(1); | |
} | |
.stack_nope .fa-remove { | |
opacity: 0.7; | |
transform: scale(1); | |
} | |
.stack--cards { | |
flex-grow: 1; | |
padding-top: 40px; | |
text-align: center; | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
align-items: center; | |
z-index: 1; | |
} | |
.stack--card { | |
display: inline-block; | |
width: 90vw; | |
max-width: 400px; | |
height: 80vh; | |
max-height: 44rem; | |
background: #FFFFFF; | |
padding-bottom: 40px; | |
border-radius: 8px; | |
overflow: hidden; | |
position: absolute; | |
will-change: transform; | |
transition: all 0.3s ease-in-out; | |
cursor: -webkit-grab; | |
cursor: -moz-grab; | |
cursor: grab; | |
} | |
.stack.overview .stack--cards { | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
align-items: flex-start; | |
overflow-y: auto; | |
} | |
.stack--card.spread { | |
position: static; | |
display: inline-block; | |
margin: 10px; | |
transform: none !important; | |
transition: all 0.3s ease-in-out; | |
} | |
.stack--card[contenteditable="true"] { | |
cursor: text; | |
box-shadow: 0 0 10px rgba(0,0,0,0.2); | |
} | |
.done{ | |
display:none | |
} | |
.moving.stack--card { | |
transition: none; | |
cursor: -webkit-grabbing; | |
cursor: -moz-grabbing; | |
cursor: grabbing; | |
} | |
.stack--card img { | |
max-width: 100%; | |
pointer-events: none; | |
} | |
.stack--card h3 { | |
margin-top: 32px; | |
font-size: 32px; | |
padding: 0 16px; | |
pointer-events: none; | |
} | |
.stack--card p { | |
margin-top: 24px; | |
font-size: 20px; | |
padding: 0 16px; | |
pointer-events: none; | |
} | |
.stack--buttons { | |
flex: 0 0 100px; | |
text-align: center; | |
padding-top: 20px; | |
} | |
.stack--buttons button { | |
border-radius: 2em; | |
border: 0; | |
background: #FFFFFF; | |
display: inline-block; | |
margin: 0 8px; | |
} | |
.stack--buttons button:focus { | |
outline: 0; | |
} | |
.stack--buttons i { | |
font-size: 32px; | |
vertical-align: middle; | |
} | |
.fa-check { | |
color: #aaFFE4; | |
} | |
.fa-remove { | |
color: #CDD6DD; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment