feat(web): rewrite home page as single-screen CLI-passthrough orchestrator
Replace multi-page task management with single-screen layout: Header with report picker at top, scrollable TaskList in the middle, and InputBar with property pills fixed at the bottom. Owns state for active report, task loading, input parsing, and task completion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,175 +2,90 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { authStore } from '$lib/stores/auth.js';
|
import { authStore } from '$lib/stores/auth.js';
|
||||||
import { tasksStore, pendingTasks, completedTasks } from '$lib/stores/tasks.js';
|
import { tasksStore } from '$lib/stores/tasks.js';
|
||||||
|
import Header from '$lib/components/Header.svelte';
|
||||||
import TaskList from '$lib/components/TaskList.svelte';
|
import TaskList from '$lib/components/TaskList.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import InputBar from '$lib/components/InputBar.svelte';
|
||||||
|
|
||||||
|
let activeReport = 'list';
|
||||||
|
/** @type {import('$lib/api/types.js').Task[]} */
|
||||||
|
let tasks = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let showCompleted = false;
|
let inputError = '';
|
||||||
|
|
||||||
$: displayTasks = showCompleted ? $completedTasks : $pendingTasks;
|
// Subscribe to store
|
||||||
|
const unsubscribe = tasksStore.subscribe(value => {
|
||||||
onMount(async () => {
|
tasks = value;
|
||||||
// Redirect to login if not authenticated
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
if (!$authStore.isAuthenticated) {
|
if (!$authStore.isAuthenticated) {
|
||||||
goto('/auth/login');
|
goto('/auth/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load tasks
|
loadReport(activeReport);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} reportName
|
||||||
|
*/
|
||||||
|
async function loadReport(reportName) {
|
||||||
|
loading = true;
|
||||||
|
inputError = '';
|
||||||
try {
|
try {
|
||||||
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
|
await tasksStore.loadReport(reportName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load tasks:', error);
|
console.error('Failed to load report:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} reportName
|
||||||
|
*/
|
||||||
|
function handleReportChange(reportName) {
|
||||||
|
activeReport = reportName;
|
||||||
|
loadReport(reportName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
*/
|
||||||
|
async function handleSubmit(input) {
|
||||||
|
inputError = '';
|
||||||
|
try {
|
||||||
|
await tasksStore.parseAndCreate(input);
|
||||||
|
} catch (error) {
|
||||||
|
inputError = error instanceof Error ? error.message : 'Failed to create task';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle task completion
|
|
||||||
* @param {string} uuid
|
* @param {string} uuid
|
||||||
*/
|
*/
|
||||||
async function handleToggle(uuid) {
|
async function handleComplete(uuid) {
|
||||||
try {
|
try {
|
||||||
await tasksStore.complete(uuid);
|
await tasksStore.complete(uuid);
|
||||||
// Reload tasks
|
|
||||||
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle task:', error);
|
console.error('Failed to complete task:', error);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to task detail
|
|
||||||
* @param {string} uuid
|
|
||||||
*/
|
|
||||||
function handleTaskClick(uuid) {
|
|
||||||
goto(`/tasks/${uuid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle between pending and completed view
|
|
||||||
*/
|
|
||||||
async function toggleView() {
|
|
||||||
showCompleted = !showCompleted;
|
|
||||||
loading = true;
|
|
||||||
try {
|
|
||||||
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load tasks:', error);
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<Header {activeReport} onReportChange={handleReportChange} />
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>Tasks</h1>
|
|
||||||
<a href="/tasks/new" class="new-btn">
|
|
||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
New
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-bar">
|
|
||||||
<button
|
|
||||||
class="filter-btn"
|
|
||||||
class:active={!showCompleted}
|
|
||||||
on:click={() => !showCompleted || toggleView()}
|
|
||||||
>
|
|
||||||
Pending ({$pendingTasks.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="filter-btn"
|
|
||||||
class:active={showCompleted}
|
|
||||||
on:click={() => showCompleted || toggleView()}
|
|
||||||
>
|
|
||||||
Completed ({$completedTasks.length})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TaskList
|
|
||||||
tasks={displayTasks}
|
|
||||||
{loading}
|
|
||||||
onToggle={handleToggle}
|
|
||||||
onTaskClick={handleTaskClick}
|
|
||||||
emptyMessage={showCompleted ? 'No completed tasks' : 'No pending tasks. Add one to get started!'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<TaskList
|
||||||
.header {
|
{tasks}
|
||||||
display: flex;
|
{loading}
|
||||||
justify-content: space-between;
|
{activeReport}
|
||||||
align-items: center;
|
onComplete={handleComplete}
|
||||||
margin-bottom: var(--spacing-lg);
|
/>
|
||||||
}
|
|
||||||
|
<InputBar
|
||||||
.header h1 {
|
onSubmit={handleSubmit}
|
||||||
margin-bottom: 0;
|
error={inputError}
|
||||||
}
|
/>
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-weight: 500;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-btn:hover {
|
|
||||||
background-color: var(--color-primary-dark);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-btn.active {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-btn:hover:not(.active) {
|
|
||||||
background-color: var(--bg-tertiary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user