Restructure mobile task manager as /tasks folder (like /admin)

Moved tasks.html to tasks/index.html
Updated 11ty to copy tasks/ folder
Now accessible at firefrostgaming.com/tasks (no .html extension needed)

Matches the pattern used by /admin for Decap CMS.
This commit is contained in:
Claude
2026-04-07 23:30:10 +00:00
parent 4352ae1021
commit b71f0dfb9d
3 changed files with 1 additions and 494 deletions

View File

@@ -8,7 +8,7 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("admin");
// Mobile task manager
eleventyConfig.addPassthroughCopy("tasks.html");
eleventyConfig.addPassthroughCopy("tasks");
return {
dir: {

493
tasks.njk
View File

@@ -1,493 +0,0 @@
---
layout: base
title: Task Manager - Firefrost Gaming
eleventyExcludeFromCollections: true
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tasks - Firefrost Gaming</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#root {
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const GITEA_API = 'https://git.firefrostgaming.com/api/v1';
const REPO_OWNER = 'firefrost-gaming';
const REPO_NAME = 'firefrost-operations-manual';
const TOKEN = 'e0e330cba1749b01ab505093a160e4423ebbbe36';
// Parse markdown frontmatter and body
const parseMarkdown = (content) => {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) return { frontmatter: {}, body: content };
const frontmatter = {};
const yamlLines = frontmatterMatch[1].split('\n');
yamlLines.forEach(line => {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
if (value === 'true') frontmatter[key] = true;
else if (value === 'false') frontmatter[key] = false;
else if (!isNaN(value) && value.trim() !== '') frontmatter[key] = parseInt(value);
else frontmatter[key] = value.trim();
}
});
return { frontmatter, body: frontmatterMatch[2] };
};
// Serialize back to markdown
const serializeMarkdown = (frontmatter, body) => {
const yamlLines = Object.entries(frontmatter).map(([key, value]) => {
return `${key}: ${value}`;
});
return `---\n${yamlLines.join('\n')}\n---\n${body}`;
};
const MobileTaskManager = () => {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedTask, setExpandedTask] = useState(null);
const [saving, setSaving] = useState(false);
const [filter, setFilter] = useState('all');
const loadTasks = async () => {
try {
setLoading(true);
const response = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/tasks?ref=master`,
{
headers: {
'Authorization': `token ${TOKEN}`,
'Accept': 'application/json'
}
}
);
if (!response.ok) throw new Error('Failed to load tasks');
const files = await response.json();
const taskFiles = files.filter(f => f.name.startsWith('task-') && f.name.endsWith('.md'));
const taskPromises = taskFiles.map(async (file) => {
const contentResponse = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${file.path}?ref=master`,
{
headers: {
'Authorization': `token ${TOKEN}`,
'Accept': 'application/json'
}
}
);
const fileData = await contentResponse.json();
const content = atob(fileData.content);
const { frontmatter, body } = parseMarkdown(content);
return {
path: file.path,
sha: fileData.sha,
name: file.name,
frontmatter,
body,
number: frontmatter.number || 0
};
});
const loadedTasks = await Promise.all(taskPromises);
const priorityOrder = { 'P0-Blocker': 0, 'P1-High': 1, 'P2-Medium': 2, 'P3-Low': 3, 'P4-Personal': 4 };
loadedTasks.sort((a, b) => {
const priorityA = priorityOrder[a.frontmatter.priority] ?? 5;
const priorityB = priorityOrder[b.frontmatter.priority] ?? 5;
if (priorityA !== priorityB) return priorityA - priorityB;
return a.number - b.number;
});
setTasks(loadedTasks);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
useEffect(() => {
loadTasks();
}, []);
const saveTask = async (task) => {
try {
setSaving(true);
const content = serializeMarkdown(task.frontmatter, task.body);
const base64Content = btoa(content);
const response = await fetch(
`${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${task.path}`,
{
method: 'PUT',
headers: {
'Authorization': `token ${TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: base64Content,
sha: task.sha,
message: `Update Task #${task.number} via mobile task manager`,
branch: 'master'
})
}
);
if (!response.ok) throw new Error('Failed to save task');
await loadTasks();
setExpandedTask(null);
setSaving(false);
} catch (err) {
alert('Error saving task: ' + err.message);
setSaving(false);
}
};
const updateTaskField = (task, field, value) => {
const updatedTask = {
...task,
frontmatter: {
...task.frontmatter,
[field]: value
}
};
saveTask(updatedTask);
};
const getPriorityColor = (priority) => {
switch (priority) {
case 'P0-Blocker': return '#dc3545';
case 'P1-High': return '#FF6B35';
case 'P1': return '#FF6B35';
case 'P2-Medium': return '#ffc107';
case 'P3-Low': return '#4ECDC4';
case 'P4-Personal': return '#A855F7';
default: return '#6c757d';
}
};
const getStatusColor = (status) => {
switch (status) {
case 'Complete': return '#28a745';
case 'In Progress': return '#4ECDC4';
case 'Blocked': return '#dc3545';
case 'Planned': return '#6c757d';
default: return '#6c757d';
}
};
const filteredTasks = tasks.filter(task => {
if (filter === 'all') return true;
if (filter === 'blocker') return task.frontmatter.blocker === true;
if (filter === 'active') return task.frontmatter.status === 'In Progress';
if (filter === 'complete') return task.frontmatter.status === 'Complete';
return true;
});
if (loading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}>🔥❄️</div>
<div>Loading tasks...</div>
</div>
);
}
if (error) {
return (
<div style={{ padding: '20px', textAlign: 'center', color: '#dc3545' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}>⚠️</div>
<div>Error: {error}</div>
<button onClick={loadTasks} style={{ marginTop: '20px', padding: '10px 20px' }}>
Retry
</button>
</div>
);
}
return (
<div style={{
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
maxWidth: '100%',
margin: '0',
padding: '0',
backgroundColor: '#f5f5f5',
minHeight: '100vh'
}}>
<div style={{
backgroundColor: '#0F0F1E',
color: 'white',
padding: '16px',
position: 'sticky',
top: 0,
zIndex: 100,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h1 style={{ margin: '0 0 12px 0', fontSize: '20px', fontWeight: '700' }}>
🔥 Firefrost Tasks
</h1>
<div style={{ display: 'flex', gap: '8px', overflowX: 'auto' }}>
{[
{ key: 'all', label: 'All', count: tasks.length },
{ key: 'blocker', label: 'Blockers', count: tasks.filter(t => t.frontmatter.blocker).length },
{ key: 'active', label: 'Active', count: tasks.filter(t => t.frontmatter.status === 'In Progress').length },
{ key: 'complete', label: 'Done', count: tasks.filter(t => t.frontmatter.status === 'Complete').length }
].map(({ key, label, count }) => (
<button
key={key}
onClick={() => setFilter(key)}
style={{
padding: '8px 16px',
borderRadius: '20px',
border: 'none',
backgroundColor: filter === key ? '#4ECDC4' : 'rgba(255,255,255,0.2)',
color: 'white',
fontSize: '14px',
fontWeight: filter === key ? '600' : '400',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
>
{label} ({count})
</button>
))}
</div>
</div>
<div style={{ padding: '12px' }}>
{filteredTasks.map(task => (
<div
key={task.path}
style={{
backgroundColor: 'white',
borderRadius: '12px',
marginBottom: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden'
}}
>
<div
onClick={() => setExpandedTask(expandedTask === task.path ? null : task.path)}
style={{
padding: '16px',
cursor: 'pointer',
borderLeft: `4px solid ${getPriorityColor(task.frontmatter.priority)}`
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
Task #{task.number}
</div>
<div style={{ fontSize: '16px', fontWeight: '600', color: '#0F0F1E' }}>
{task.frontmatter.title}
</div>
</div>
<div style={{ fontSize: '20px', marginLeft: '8px' }}>
{expandedTask === task.path ? '▼' : '▶'}
</div>
</div>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: getPriorityColor(task.frontmatter.priority),
color: 'white'
}}>
{task.frontmatter.priority}
</span>
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: getStatusColor(task.frontmatter.status),
color: 'white'
}}>
{task.frontmatter.status}
</span>
{task.frontmatter.blocker && (
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '600',
backgroundColor: '#dc3545',
color: 'white'
}}>
🚨 BLOCKER
</span>
)}
<span style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
backgroundColor: '#e9ecef',
color: '#495057'
}}>
{task.frontmatter.owner}
</span>
</div>
</div>
{expandedTask === task.path && (
<div style={{
padding: '16px',
borderTop: '1px solid #e9ecef',
backgroundColor: '#f8f9fa'
}}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Status
</label>
<select
value={task.frontmatter.status}
onChange={(e) => updateTaskField(task, 'status', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="Planned">Planned</option>
<option value="In Progress">In Progress</option>
<option value="Blocked">Blocked</option>
<option value="Complete">Complete</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Priority
</label>
<select
value={task.frontmatter.priority}
onChange={(e) => updateTaskField(task, 'priority', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="P0-Blocker">P0 - Blocker</option>
<option value="P1-High">P1 - High</option>
<option value="P2-Medium">P2 - Medium</option>
<option value="P3-Low">P3 - Low</option>
<option value="P4-Personal">P4 - Personal</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '8px', color: '#495057' }}>
Owner
</label>
<select
value={task.frontmatter.owner}
onChange={(e) => updateTaskField(task, 'owner', e.target.value)}
disabled={saving}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #dee2e6',
backgroundColor: 'white'
}}
>
<option value="Michael">Michael</option>
<option value="Meg">Meg</option>
<option value="Holly">Holly</option>
<option value="Trinity">Trinity</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', fontSize: '14px', fontWeight: '600', color: '#495057' }}>
<input
type="checkbox"
checked={task.frontmatter.blocker === true}
onChange={(e) => updateTaskField(task, 'blocker', e.target.checked)}
disabled={saving}
style={{ marginRight: '8px', width: '20px', height: '20px' }}
/>
Launch Blocker
</label>
</div>
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: 'white', borderRadius: '8px', fontSize: '14px', lineHeight: '1.6', color: '#495057', whiteSpace: 'pre-wrap' }}>
{task.body.split('\n').slice(0, 5).join('\n')}
{task.body.split('\n').length > 5 && <div style={{ marginTop: '8px', fontStyle: 'italic', color: '#6c757d' }}>...</div>}
</div>
{saving && (
<div style={{ marginTop: '12px', textAlign: 'center', color: '#4ECDC4', fontSize: '14px' }}>
💾 Saving...
</div>
)}
</div>
)}
</div>
))}
{filteredTasks.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#6c757d' }}>
No tasks found
</div>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<MobileTaskManager />);
</script>
</body>
</html>