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 => {
|
||||||
|
tasks = value;
|
||||||
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
// Redirect to login if not authenticated
|
|
||||||
if (!$authStore.isAuthenticated) {
|
if (!$authStore.isAuthenticated) {
|
||||||
goto('/auth/login');
|
goto('/auth/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load tasks
|
loadReport(activeReport);
|
||||||
try {
|
|
||||||
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
|
return unsubscribe;
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load tasks:', error);
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle task completion
|
* @param {string} reportName
|
||||||
* @param {string} uuid
|
|
||||||
*/
|
*/
|
||||||
async function handleToggle(uuid) {
|
async function loadReport(reportName) {
|
||||||
|
loading = true;
|
||||||
|
inputError = '';
|
||||||
try {
|
try {
|
||||||
await tasksStore.complete(uuid);
|
await tasksStore.loadReport(reportName);
|
||||||
// Reload tasks
|
|
||||||
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle task:', error);
|
console.error('Failed to load report:', error);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to task detail
|
* @param {string} reportName
|
||||||
* @param {string} uuid
|
|
||||||
*/
|
*/
|
||||||
function handleTaskClick(uuid) {
|
function handleReportChange(reportName) {
|
||||||
goto(`/tasks/${uuid}`);
|
activeReport = reportName;
|
||||||
|
loadReport(reportName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle between pending and completed view
|
* @param {string} input
|
||||||
*/
|
*/
|
||||||
async function toggleView() {
|
async function handleSubmit(input) {
|
||||||
showCompleted = !showCompleted;
|
inputError = '';
|
||||||
loading = true;
|
|
||||||
try {
|
try {
|
||||||
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
|
await tasksStore.parseAndCreate(input);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load tasks:', error);
|
inputError = error instanceof Error ? error.message : 'Failed to create task';
|
||||||
} finally {
|
}
|
||||||
loading = false;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} uuid
|
||||||
|
*/
|
||||||
|
async function handleComplete(uuid) {
|
||||||
|
try {
|
||||||
|
await tasksStore.complete(uuid);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to complete task:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</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">
|
<TaskList
|
||||||
<button
|
{tasks}
|
||||||
class="filter-btn"
|
{loading}
|
||||||
class:active={!showCompleted}
|
{activeReport}
|
||||||
on:click={() => !showCompleted || toggleView()}
|
onComplete={handleComplete}
|
||||||
>
|
/>
|
||||||
Pending ({$pendingTasks.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="filter-btn"
|
|
||||||
class:active={showCompleted}
|
|
||||||
on:click={() => showCompleted || toggleView()}
|
|
||||||
>
|
|
||||||
Completed ({$completedTasks.length})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TaskList
|
<InputBar
|
||||||
tasks={displayTasks}
|
onSubmit={handleSubmit}
|
||||||
{loading}
|
error={inputError}
|
||||||
onToggle={handleToggle}
|
/>
|
||||||
onTaskClick={handleTaskClick}
|
|
||||||
emptyMessage={showCompleted ? 'No completed tasks' : 'No pending tasks. Add one to get started!'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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