Files
firefrost-website/admin/tasks.html
Claude 3f6d84466f Add task manager to admin folder as workaround
Since /tasks folder not deploying, putting it in /admin (which we know works).
Access at: firefrostgaming.com/admin/tasks.html
2026-04-07 23:40:13 +00:00

486 lines
20 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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';
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] };
};
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, 'P1': 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', fontSize: '16px', borderRadius: '8px', border: 'none', backgroundColor: '#4ECDC4', color: 'white', cursor: 'pointer' }}>
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', paddingBottom: '4px' }}>
{[
{ 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>