Code Generator
Configure and copy your Circle.so code
Enable for homework/assignments (creates separate tracking)
Auto-generated values:
Action ID: Class-0001_attendance
Field Name: Class-0001_attended
<!-- Copy everything below this line into your Circle.so post HTML field -->
<div class="checkbox-wrapper" id="checkbox-wrapper-Class-0001_attendance">
<div class="checkbox-container">
<input type="checkbox" class="hidden-checkbox" id="task-Class-0001_attendance">
<label class="custom-checkbox" for="task-Class-0001_attendance">
<svg class="checkmark" width="13" height="11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.55.89a1.437 1.437 0 0 1 0 2.032L5.361 10.11a1.437 1.437 0 0 1-2.033 0L.454 7.235a1.438 1.438 0 0 1 2.033-2.033l1.859 1.86L10.516.89a1.437 1.437 0 0 1 2.033 0Z" fill="#fff"/>
</svg>
</label>
<span class="label">Watched Today's Lesson! 👍</span>
</div>
<div class="PTS-badge">
<svg class="star-icon" width="12" height="12" fill="currentColor">
<path d="M11.963 4.563a.597.597 0 00-.325-.35c-.6-.263-2.175-.363-3.538-.413C7.675 2.563 6.988.737 6.412.175A.588.588 0 006 0a.64.64 0 00-.425.175C5.012.737 4.325 2.562 3.9 3.8c-1.362.05-2.938.15-3.538.413a.562.562 0 00-.325.35.585.585 0 00.038.5c.388.712 1.375 1.775 2.612 2.825-.337 1.374-.562 2.6-.624 3.462a.606.606 0 00.274.55.577.577 0 00.6.037c.213-.124.45-.237.688-.35.65-.325 1.438-.687 2.375-1.274.938.587 1.725.95 2.375 1.274.238.113.475.226.688.35a.681.681 0 00.274.063.62.62 0 00.325-.1.606.606 0 00.275-.55c-.062-.862-.287-2.088-.624-3.462 1.237-1.05 2.224-2.113 2.612-2.825.087-.15.1-.338.037-.5z"/>
</svg>
2 PTS
</div>
</div>
<div id="toast-Class-0001_attendance" class="toast"></div>
<canvas id="confetti-canvas"></canvas>
<style>
/* Reset any inherited colors */
.checkbox-wrapper,
.checkbox-wrapper * {
color: #6c8357;
}
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
font-family: system-ui, -apple-system, sans-serif;
margin-top: 16px;
}
.checkbox-container {
position: relative;
cursor: pointer;
padding: 6px;
margin-left: -6px;
display: flex;
align-items: center;
}
.hidden-checkbox {
display: none;
}
.custom-checkbox {
width: 24px;
height: 24px;
border: 2px solid #6c8357;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.checkmark {
opacity: 0;
transform: scale(0.8) rotate(-45deg);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.checkmark path {
fill: #fff !important;
}
.hidden-checkbox:checked + .custom-checkbox {
background-color: #6c8357;
border-color: #6c8357;
animation: pulse 0.5s;
}
.hidden-checkbox:checked + .custom-checkbox .checkmark {
opacity: 1;
transform: scale(1) rotate(0);
}
.hidden-checkbox:checked + .custom-checkbox::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.7);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: burst 0.5s;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes burst {
0% { width: 0; height: 0; opacity: 0.5; }
100% { width: 200%; height: 200%; opacity: 0; }
}
.label {
font-weight: 600;
font-size: 14px;
color: #6c8357 !important;
margin-left: 8px;
transition: all 0.3s;
text-decoration: none !important;
}
.checkbox-container:hover .label {
color: #6c8357 !important;
}
.hidden-checkbox:checked ~ .label {
animation: labelPop 0.3s forwards;
color: #6c8357 !important;
}
@keyframes labelPop {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.PTS-badge {
display: flex;
align-items: center;
background-color: #6c8357;
color: white !important;
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
border-radius: 12px;
border: 1px solid #6c8357;
margin-left: 8px;
transition: all 0.3s;
}
.PTS-badge * {
color: white !important;
}
.hidden-checkbox:checked ~ .PTS-badge {
animation: badgePop 0.3s forwards;
}
@keyframes badgePop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.star-icon {
margin-right: 4px;
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 8px;
color: white !important;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 9999;
font-size: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
pointer-events: none;
}
.toast * {
color: white !important;
}
.toast.success {
background-color: #6c8357;
}
.toast.error {
background-color: #EF4444;
}
.toast.show {
opacity: 1;
animation: toastPop 0.3s forwards;
}
@keyframes toastPop {
0% { transform: translateX(-50%) scale(0.9); }
100% { transform: translateX(-50%) scale(1); }
}
#confetti-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9998;
}
/* Force color overrides */
.checkbox-wrapper a,
.checkbox-wrapper a:hover,
.checkbox-wrapper a:visited {
color: #6c8357 !important;
text-decoration: none !important;
}
.custom-checkbox:hover {
border-color: #6c8357 !important;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
<script>
(function() {
// —-------------------- CONFIGURE THESE VALUES FOR EACH POST —----------
const CLASS_ID = 'Class-0001'; // Add a class identifier here
const ACTION_ID = 'Class-0001_attendance'; // Change this for each post (and in the HTML above!)
const FIELD_NAME = 'Class-0001_attended'; // Change this for each post
const POINTS = 2; // Points to award
const WEBHOOK_URL = 'https://hook.us1.make.com/7e2upld7vea5xgb1f80q9en29wulvtjy';
const SOUND_URL = 'https://assets-v2.circle.so/1ss93jmz4w3admahhsaufz39zglr';
function showToast(message, type = 'success') {
console.log('Showing toast:', message, type, 'for action:', ACTION_ID);
const toast = document.getElementById(`toast-${ACTION_ID}`);
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
function playCheckSound() {
const audio = new Audio(SOUND_URL);
audio.play().catch(error => {
console.error('Error playing sound:', error);
});
}
function triggerConfetti() {
const duration = 3 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
const interval = setInterval(function() {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
const particleCount = 50 * (timeLeft / duration);
confetti(Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
}));
confetti(Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
}));
}, 250);
}
async function updatePoints(checked) {
console.log('Updating points:', checked, 'for action:', ACTION_ID);
try {
const payload = {
classId: CLASS_ID, // Include the class ID here
email: circleUser.email,
action: checked ? 'add' : 'subtract',
points: POINTS,
actionId: ACTION_ID,
[FIELD_NAME]: checked ? 'checked' : 'unchecked'
};
console.log('Sending webhook payload:', payload);
console.log('Circle User:', circleUser);
const response = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
console.log('Webhook response status:', response.status);
const responseData = await response.json().catch(e => console.log('No JSON response'));
console.log('Webhook response data:', responseData);
if (!response.ok) {
throw new Error(`Failed to update points: ${response.status} ${response.statusText}`);
}
localStorage.setItem(`checkbox_state_${circleUser.email}_${ACTION_ID}`, checked);
showToast(checked ? `+${POINTS} PTS Added!` : `-${POINTS} PTS Removed`, 'success');
if (checked) {
triggerConfetti();
}
} catch (error) {
console.error('Error updating points:', error);
console.error('Error details:', {
email: circleUser?.email,
ACTION_ID,
FIELD_NAME,
WEBHOOK_URL
});
const checkbox = document.getElementById(`task-${ACTION_ID}`);
if (checkbox) checkbox.checked = !checked;
showToast('Failed to update points. Please try again.', 'error');
}
}
function loadSavedState() {
console.log('Loading saved state for action:', ACTION_ID);
const checkbox = document.getElementById(`task-${ACTION_ID}`);
if (!checkbox) return;
const savedState = localStorage.getItem(`checkbox_state_${circleUser.email}_${ACTION_ID}`);
console.log('Saved state for action', ACTION_ID, ':', savedState);
if (savedState !== null) {
checkbox.checked = savedState === 'true';
}
}
function initialize() {
console.log('Initializing checkbox for action:', ACTION_ID);
try {
if (!window.circleUser) {
throw new Error('Circle user data not found');
}
console.log('Circle User found:', circleUser);
const checkbox = document.getElementById(`task-${ACTION_ID}`);
if (!checkbox) {
throw new Error('Checkbox element not found');
}
loadSavedState();
checkbox.addEventListener('change', async (e) => {
console.log('Checkbox changed:', e.target.checked, 'for action:', ACTION_ID);
if (e.target.checked) {
playCheckSound();
}
await updatePoints(e.target.checked);
});
console.log('Checkbox initialized successfully for action:', ACTION_ID);
} catch (error) {
console.error('Error initializing checkbox:', error);
showToast('Failed to initialize checkbox', 'error');
}
}
// Initialize when the DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();
</script>
<!-- Copy everything above this line into your Circle.so post HTML field -->