Skip to content

Commit 2b433ba

Browse files
committed
holy ai genereated fuzzy date matcher
1 parent 9332570 commit 2b433ba

File tree

4 files changed

+668
-38
lines changed

4 files changed

+668
-38
lines changed

src/lib/components/AddTask.svelte

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte'
3+
4+
export let value = ''
5+
export let parsed = null
6+
export let placeholder = 'new task'
7+
export let disabled = false
8+
export let loading = false
9+
10+
const dispatch = createEventDispatcher()
11+
12+
const handleSubmit = (event) => {
13+
event.preventDefault()
14+
dispatch('submit')
15+
}
16+
17+
const handleInput = (event) => {
18+
dispatch('input', event.target.value)
19+
}
20+
21+
$: match = parsed?.match
22+
$: before = match ? value.slice(0, match.start) : value
23+
$: matchedText = match ? value.slice(match.start, match.end) : ''
24+
$: after = match ? value.slice(match.end) : ''
25+
$: showPlaceholder = !value
26+
</script>
27+
28+
<form class="add-task-form" onsubmit={handleSubmit}>
29+
<span class="dark">+</span>
30+
<div class="input-shell">
31+
<div class="input-overlay" aria-hidden="true">
32+
{#if showPlaceholder}
33+
<span class="placeholder">{placeholder}</span>
34+
{:else if match}
35+
<span>{before}</span><span class="date-highlight"
36+
>{matchedText}</span
37+
><span>{after}</span>
38+
{:else}
39+
<span>{value}</span>
40+
{/if}
41+
</div>
42+
<input
43+
class="add-task-input"
44+
type="text"
45+
bind:value
46+
{placeholder}
47+
oninput={handleInput}
48+
disabled={disabled || loading}
49+
aria-label="Add task"
50+
autocomplete="off"
51+
/>
52+
</div>
53+
</form>
54+
55+
<style>
56+
.add-task-form {
57+
opacity: 0;
58+
display: flex;
59+
gap: 1ch;
60+
transition: opacity 0.2s ease;
61+
flex: 1;
62+
align-items: center;
63+
}
64+
.add-task-form:hover,
65+
.add-task-form:focus-within {
66+
opacity: 1;
67+
}
68+
.input-shell {
69+
position: relative;
70+
flex: 1;
71+
min-width: 0;
72+
}
73+
.input-overlay {
74+
position: absolute;
75+
inset: 0;
76+
pointer-events: none;
77+
white-space: pre;
78+
overflow: hidden;
79+
font: inherit;
80+
color: var(--txt-2);
81+
line-height: 1.5;
82+
}
83+
.placeholder {
84+
color: var(--txt-3);
85+
}
86+
.date-highlight {
87+
color: var(--txt-1);
88+
}
89+
.add-task-input {
90+
flex: 1;
91+
background: transparent;
92+
padding: 0;
93+
border: none;
94+
color: transparent;
95+
caret-color: var(--txt-1);
96+
height: 1.5rem;
97+
width: 100%;
98+
}
99+
.add-task-input::placeholder {
100+
color: transparent;
101+
}
102+
.add-task-input:disabled {
103+
opacity: 0.5;
104+
}
105+
</style>

src/lib/components/Todoist.svelte

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
import { onMount, onDestroy, untrack } from 'svelte'
33
import TodoistAPI from '../todoist-api.js'
44
import { settings } from '../settings-store.svelte.js'
5+
import AddTask from './AddTask.svelte'
6+
import {
7+
parseSmartDate,
8+
stripDateMatch,
9+
formatTodoistDue,
10+
} from '../date-matcher.js'
511
612
let api = null
713
let tasks = $state([])
@@ -11,6 +17,7 @@
1117
let taskCount = $derived(tasks.filter((task) => !task.checked).length)
1218
let newTaskContent = $state('')
1319
let addingTask = $state(false)
20+
let parsedDate = $state(null)
1421
1522
function handleVisibilityChange() {
1623
if (document.visibilityState === 'visible' && api) {
@@ -29,6 +36,14 @@
2936
initializeAPI(token, true)
3037
})
3138
39+
$effect(() => {
40+
const parsed = parseSmartDate(newTaskContent)
41+
parsedDate = parsed
42+
if (parsed) {
43+
console.log('Parsed date:', parsed)
44+
}
45+
})
46+
3247
async function initializeAPI(token, clearLocalData = false) {
3348
if (!token) {
3449
api = null
@@ -61,11 +76,20 @@
6176
6277
async function addTask(event) {
6378
event.preventDefault()
64-
if (!newTaskContent.trim() || !api || addingTask) return
79+
const raw = newTaskContent.trim()
80+
if (!raw || !api || addingTask) return
6581
82+
const parsed = parsedDate || parseSmartDate(raw)
83+
let content = raw
84+
let due = null
85+
if (parsed?.match) {
86+
const cleaned = stripDateMatch(raw, parsed.match)
87+
content = cleaned || raw
88+
due = formatTodoistDue(parsed.date, parsed.hasTime)
89+
}
6690
try {
6791
addingTask = true
68-
await api.addTask(newTaskContent.trim())
92+
await api.addTask(content, due)
6993
newTaskContent = ''
7094
await loadTasks()
7195
} catch (err) {
@@ -204,16 +228,13 @@
204228
? ''
205229
: 's'}
206230
</a>
207-
<form onsubmit={addTask}>
208-
<span class="dark">+</span>
209-
<input
210-
type="text"
211-
class="add-task-input"
212-
placeholder="new task"
213-
bind:value={newTaskContent}
214-
disabled={addingTask}
215-
/>
216-
</form>
231+
<AddTask
232+
bind:value={newTaskContent}
233+
parsed={parsedDate}
234+
disabled={addingTask}
235+
loading={addingTask}
236+
on:submit={addTask}
237+
/>
217238
</div>
218239
219240
<br />
@@ -289,29 +310,4 @@
289310
.overdue-date {
290311
color: var(--txt-err);
291312
}
292-
form {
293-
opacity: 0;
294-
display: flex;
295-
gap: 1ch;
296-
transition: opacity 0.2s ease;
297-
flex: 1;
298-
}
299-
form:hover,
300-
form:focus-within {
301-
opacity: 1;
302-
}
303-
.add-task-input {
304-
flex: 1;
305-
background: transparent;
306-
padding: 0;
307-
border: none;
308-
color: var(--txt-2);
309-
height: 1.5rem;
310-
}
311-
.add-task-input::placeholder {
312-
color: var(--txt-3);
313-
}
314-
.add-task-input:disabled {
315-
opacity: 0.5;
316-
}
317313
</style>

0 commit comments

Comments
 (0)