decommission: Remove Decap CMS entirely
Decap CMS didn't work for any of us — clunky UI, exposed YAML frontmatter, bad mobile experience. Task management moved to PostgreSQL + Discord ChatOps + Trinity Console. Document browsing happens via Gitea directly. Removed: admin/config.yml, admin/index.html, admin/mobile.html, admin/tasks.html Chronicler #78 | firefrost-website
This commit is contained in:
384
admin/config.yml
384
admin/config.yml
@@ -1,384 +0,0 @@
|
||||
backend:
|
||||
name: gitea
|
||||
repo: firefrost-gaming/firefrost-operations-manual
|
||||
api_root: https://git.firefrostgaming.com/api/v1
|
||||
base_url: https://git.firefrostgaming.com
|
||||
app_id: ad439d72-e724-4f88-ad24-a1187c52b313
|
||||
use_pkce: true
|
||||
branch: master
|
||||
auth_endpoint: login/oauth/authorize
|
||||
token_endpoint: login/oauth/access_token
|
||||
|
||||
# Site settings
|
||||
site_url: https://firefrostgaming.com
|
||||
display_url: https://firefrostgaming.com
|
||||
logo_url: /assets/images/2026/02/Light-logo.png
|
||||
|
||||
# Disable preview pane globally (dark mode compatibility - font/background color issue)
|
||||
editor:
|
||||
preview: false
|
||||
|
||||
# Media library points to branding assets
|
||||
media_folder: "docs/branding"
|
||||
public_folder: "/branding"
|
||||
|
||||
collections:
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# CORE DOCUMENTS (Single Files)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "core_docs"
|
||||
label: "📌 Core Documents"
|
||||
files:
|
||||
- label: "Session Handoff"
|
||||
name: "handoff"
|
||||
file: "SESSION-HANDOFF-NEXT.md"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- label: "Infrastructure Manifest"
|
||||
name: "infrastructure"
|
||||
file: "docs/core/infrastructure-manifest.md"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- label: "Document Index"
|
||||
name: "doc-index"
|
||||
file: "DOCUMENT-INDEX.md"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# PLANNING & STRATEGY
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "planning"
|
||||
label: "📋 Planning"
|
||||
folder: "docs/planning"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "milestones"
|
||||
label: "🏆 Milestones"
|
||||
folder: "docs/milestones"
|
||||
create: true
|
||||
extension: "md"
|
||||
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
sortable_fields: ["filename"]
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "vision"
|
||||
label: "🔮 Vision"
|
||||
folder: "docs/vision"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# TECHNICAL DOCUMENTATION
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "implementation"
|
||||
label: "🔧 Implementation Guides"
|
||||
folder: "docs/implementation"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "infrastructure"
|
||||
label: "🖥️ Infrastructure"
|
||||
folder: "docs/infrastructure"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "deployment"
|
||||
label: "🚀 Deployment"
|
||||
folder: "docs/deployment"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "services"
|
||||
label: "⚙️ Services"
|
||||
folder: "docs/services"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "troubleshooting"
|
||||
label: "🔍 Troubleshooting"
|
||||
folder: "docs/troubleshooting"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# PROCEDURES & STANDARDS
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "procedures"
|
||||
label: "📝 Procedures"
|
||||
folder: "docs/procedures"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "standards"
|
||||
label: "📏 Standards"
|
||||
folder: "docs/standards"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "guides"
|
||||
label: "📖 Guides"
|
||||
folder: "docs/guides"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "templates"
|
||||
label: "📄 Templates"
|
||||
folder: "docs/templates"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# RELATIONSHIP & CHRONICLERS
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "relationship"
|
||||
label: "💜 Relationship"
|
||||
folder: "docs/relationship"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "sessions"
|
||||
label: "📅 Sessions"
|
||||
folder: "docs/sessions"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# MARKETING & SOCIAL
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "marketing"
|
||||
label: "📣 Marketing"
|
||||
folder: "docs/marketing"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "social-media"
|
||||
label: "📱 Social Media"
|
||||
folder: "docs/social-media"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "branding"
|
||||
label: "🎨 Branding"
|
||||
folder: "docs/branding"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "branding_assets"
|
||||
label: "🎨 Branding Assets"
|
||||
files:
|
||||
- label: "Trinity Final (PNG)"
|
||||
name: "trinity-final"
|
||||
file: "docs/branding/trinity-final.png"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "Trinity Fixed (WebP)"
|
||||
name: "trinity-fixed"
|
||||
file: "docs/branding/trinity-fixed.webp"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "Trinity Image (WebP)"
|
||||
name: "trinity-image"
|
||||
file: "docs/branding/trinity-image.webp"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "YouTube Banner"
|
||||
name: "youtube-banner"
|
||||
file: "docs/branding/youtube-banner-2560x1440.png"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "YouTube Banner (Minecraft)"
|
||||
name: "youtube-banner-minecraft"
|
||||
file: "docs/branding/youtube-banner-minecraft-2560x1440.png"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- name: "trinity_skins"
|
||||
label: "🎮 Trinity Skins"
|
||||
files:
|
||||
- label: "Frost Wizard (Frostystyle)"
|
||||
name: "frost-wizard"
|
||||
file: "docs/branding/trinity-skins/frost-wizard-frostystyle.png"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "Fire Emissary (Gingerfury)"
|
||||
name: "fire-emissary"
|
||||
file: "docs/branding/trinity-skins/fire-emissary-gingerfury.png"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "Arcane Catalyst (Unicorn20089)"
|
||||
name: "arcane-catalyst"
|
||||
file: "docs/branding/trinity-skins/arcane-catalyst-unicorn20089.png"
|
||||
fields:
|
||||
- {label: "Image", name: "image", widget: "image"}
|
||||
|
||||
- label: "Skin Viewer"
|
||||
name: "skin-viewer"
|
||||
file: "docs/branding/trinity-skins/minecraft_skin_viewer.html"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "code"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# LEGAL & EMERGENCY
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "legal"
|
||||
label: "⚖️ Legal"
|
||||
folder: "docs/legal"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "emergency"
|
||||
label: "🚨 Emergency Protocols"
|
||||
folder: "docs/emergency-protocols"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# REFERENCE & RESEARCH
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "reference"
|
||||
label: "📚 Reference"
|
||||
folder: "docs/reference"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "research"
|
||||
label: "🔬 Research"
|
||||
folder: "docs/research"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "consultations"
|
||||
label: "💬 Consultations"
|
||||
folder: "docs/consultations"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# TOOLS & TRAINING
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
- name: "tools"
|
||||
label: "🛠️ Tools"
|
||||
folder: "docs/tools"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "training"
|
||||
label: "🎓 Training"
|
||||
folder: "docs/training"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
|
||||
- name: "learning"
|
||||
label: "📝 Learning"
|
||||
folder: "docs/learning"
|
||||
create: true
|
||||
extension: "md"
|
||||
identifier_field: "name"
|
||||
summary: "{{filename}}"
|
||||
fields:
|
||||
- {label: "Content", name: "body", widget: "markdown"}
|
||||
223
admin/index.html
223
admin/index.html
@@ -1,223 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Firefrost CMS</title>
|
||||
<style>
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
FIREFROST GAMING - DECAP CMS MOBILE OPTIMIZATIONS
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Basic mobile viewport setup */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
📱 MOBILE RESPONSIVE OPTIMIZATIONS
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Sidebar - Show in main content area on mobile (not hidden) */
|
||||
aside,
|
||||
[class*="Sidebar"],
|
||||
nav[class*="Sidebar"] {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
/* Collections list items - More compact */
|
||||
aside li,
|
||||
aside a,
|
||||
[class*="Sidebar"] li,
|
||||
[class*="Sidebar"] a {
|
||||
padding: 8px 12px !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.4 !important;
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
/* Collections heading - Smaller */
|
||||
aside h2,
|
||||
[class*="Sidebar"] h2 {
|
||||
font-size: 18px !important;
|
||||
margin: 8px 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Search box - More compact */
|
||||
aside input,
|
||||
[class*="Sidebar"] input {
|
||||
padding: 8px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Main content takes full width */
|
||||
main,
|
||||
[class*="MainContent"],
|
||||
[class*="Content"] {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
padding: 16px 12px !important;
|
||||
}
|
||||
|
||||
/* Header adjustments */
|
||||
header,
|
||||
[class*="AppHeader"] {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* Collection cards - Stack vertically, larger touch targets */
|
||||
[class*="ListCard"],
|
||||
[class*="EntryCard"],
|
||||
li[class*="Entry"] {
|
||||
padding: 16px 12px !important;
|
||||
margin: 12px 0 !important;
|
||||
min-height: 60px !important;
|
||||
}
|
||||
|
||||
/* Buttons - Larger touch targets */
|
||||
button {
|
||||
min-height: 44px !important;
|
||||
padding: 12px 20px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* Input fields - Larger, easier to tap */
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
min-height: 44px !important;
|
||||
font-size: 16px !important;
|
||||
padding: 10px 12px !important;
|
||||
}
|
||||
|
||||
/* Text areas - More vertical space */
|
||||
textarea {
|
||||
min-height: 120px !important;
|
||||
}
|
||||
|
||||
/* Search box */
|
||||
input[type="search"],
|
||||
input[placeholder*="Search"] {
|
||||
width: 100% !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
/* Collection grid - Single column on mobile */
|
||||
[class*="CardGrid"],
|
||||
[class*="Grid"] {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
/* Editor - Full width, no side-by-side */
|
||||
[class*="EditorContainer"],
|
||||
[class*="SplitPane"] {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
[class*="ControlPane"],
|
||||
[class*="PreviewPane"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Toolbar - Wrap items, don't overflow */
|
||||
[class*="Toolbar"],
|
||||
[class*="EditorToolbar"] {
|
||||
flex-wrap: wrap !important;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
[class*="Toolbar"] button {
|
||||
margin: 4px !important;
|
||||
}
|
||||
|
||||
/* Dropdowns and modals - Full width */
|
||||
[class*="Dropdown"],
|
||||
[class*="Modal"],
|
||||
[class*="Dialog"] {
|
||||
max-width: 95vw !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* Collection header */
|
||||
h1 {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
/* List items - Better spacing */
|
||||
ul[class*="List"] > li {
|
||||
margin: 12px 0 !important;
|
||||
}
|
||||
|
||||
/* Reduce padding on small screens */
|
||||
[class*="Container"],
|
||||
[class*="Wrapper"] {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* Form fields - Stack vertically */
|
||||
[class*="FormControl"],
|
||||
[class*="FieldWrapper"] {
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
/* Labels - Clearer */
|
||||
label {
|
||||
font-size: 14px !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 8px !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Extra small devices - even more compact */
|
||||
body {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
/* Reduce all padding */
|
||||
main,
|
||||
[class*="MainContent"] {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
[class*="ListCard"],
|
||||
[class*="EntryCard"] {
|
||||
padding: 12px 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,485 +0,0 @@
|
||||
<!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>
|
||||
485
admin/tasks.html
485
admin/tasks.html
@@ -1,485 +0,0 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user