diff --git a/src/App.tsx b/src/App.tsx index 0b12ea2..01be75d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,7 @@ import { type Component, createSignal, Show, onMount, onCleanup, lazy, Suspense } from 'solid-js'; import { Layout } from './components/Layout'; +import { EmbedLayout } from './components/EmbedLayout'; +import { EmbedAuthWrapper } from './components/EmbedAuthWrapper'; import { CriticalView } from './views/CriticalView'; import { AuthCallback } from './components/Auth'; import { pb } from './lib/pocketbase'; @@ -14,6 +16,8 @@ const SnowballView = lazy(() => import('./views/SnowballView').then(m => ({ defa const DigInView = lazy(() => import('./views/DigInView').then(m => ({ default: m.DigInView }))); const ProgressView = lazy(() => import('./views/ProgressView').then(m => ({ default: m.ProgressView }))); const HelpView = lazy(() => import('./views/HelpView').then(m => ({ default: m.HelpView }))); +const EmbedNotesView = lazy(() => import('./views/EmbedNotesView').then(m => ({ default: m.EmbedNotesView }))); +const EmbedQuickAddView = lazy(() => import('./views/EmbedQuickAddView').then(m => ({ default: m.EmbedQuickAddView }))); const App: Component = () => { // Basic routing state @@ -52,21 +56,48 @@ const App: Component = () => { }); }); + // Determine if we are in an embed route + const isEmbed = () => window.location.pathname.startsWith('/embed/'); + const embedView = () => { + const path = window.location.pathname; + if (path.includes('/notes')) return 'embed_notes'; + if (path.includes('/quick-add')) return 'embed_quick_add'; + return null; + }; + return ( - { }} />}> - -
}> - - - - - - - - - setCurrentView("settings")} /> -
-
+ { }} />}> + +
}> + + + + + + + + + setCurrentView("settings")} /> +
+
+
+ } + > + + +
}> + + + + + + +
+
+
); }; diff --git a/src/components/EmbedAuthWrapper.tsx b/src/components/EmbedAuthWrapper.tsx new file mode 100644 index 0000000..631baf4 --- /dev/null +++ b/src/components/EmbedAuthWrapper.tsx @@ -0,0 +1,51 @@ +import { type Component, type JSX, onMount, onCleanup, createSignal, Show } from "solid-js"; +import { pb } from "@/lib/pocketbase"; +import { initStore } from "@/store"; + +interface EmbedAuthWrapperProps { + children: JSX.Element; +} + +export const EmbedAuthWrapper: Component = (props) => { + const [isHydrated, setIsHydrated] = createSignal(pb.authStore.isValid); + + const handleMessage = (event: MessageEvent) => { + // You might want to add origin validation here if you know the sibling app domains + // if (event.origin !== "https://sibling-app.com") return; + + const { data } = event; + if (data?.type === "TASGRID_AUTH" && data.token) { + console.log("Received TasGrid auth payload via postMessage"); + + // Hydrate PocketBase auth store + pb.authStore.save(data.token, data.user || null); + + // Re-initialize store with new auth context + initStore().then(() => { + setIsHydrated(true); + }); + } + }; + + onMount(() => { + window.addEventListener("message", handleMessage); + }); + + onCleanup(() => { + window.removeEventListener("message", handleMessage); + }); + + return ( + +
+

Waiting for authentication...

+ + } + > + {props.children} +
+ ); +}; diff --git a/src/components/EmbedLayout.tsx b/src/components/EmbedLayout.tsx new file mode 100644 index 0000000..60a75d6 --- /dev/null +++ b/src/components/EmbedLayout.tsx @@ -0,0 +1,17 @@ +import { type Component, type JSX } from "solid-js"; + +interface EmbedLayoutProps { + children: JSX.Element; +} + +export const EmbedLayout: Component = (props) => { + return ( +
+
+
+ {props.children} +
+
+
+ ); +}; diff --git a/src/views/EmbedNotesView.tsx b/src/views/EmbedNotesView.tsx new file mode 100644 index 0000000..a5a1ecb --- /dev/null +++ b/src/views/EmbedNotesView.tsx @@ -0,0 +1,15 @@ +import { type Component, createSignal } from "solid-js"; +import { NotepadView } from "./NotepadView"; + +export const EmbedNotesView: Component = () => { + const [selectedNoteId, setSelectedNoteId] = createSignal(null); + + return ( +
+ +
+ ); +}; diff --git a/src/views/EmbedQuickAddView.tsx b/src/views/EmbedQuickAddView.tsx new file mode 100644 index 0000000..929ad2b --- /dev/null +++ b/src/views/EmbedQuickAddView.tsx @@ -0,0 +1,292 @@ +import { type Component, createSignal, createEffect, onMount, For, Show } from "solid-js"; +import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate } from "@/store"; +import { Search, Clock, ArrowUpCircle, Copy, ChevronDown, Gauge } from "lucide-solid"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { TextField, TextFieldInput } from "@/components/ui/textfield"; +import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; +import { TagPicker } from "@/components/TagPicker"; +import { useTheme } from "@/components/ThemeProvider"; +import { getThemeAdjustedColor } from "@/lib/colors"; +import { lazy, Suspense } from "solid-js"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { toast } from "solid-sonner"; +import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants"; +import { cn } from "@/lib/utils"; + +const TaskEditor = lazy(() => import("@/components/TaskEditor").then(m => ({ default: m.TaskEditor }))); + +export const EmbedQuickAddView: Component = () => { + const { resolvedTheme } = useTheme(); + let lastParsedTags: string[] = []; + + const [input, setInput] = createSignal(""); + const [priority, setPriority] = createSignal("5"); + const [urgency, setUrgency] = createSignal("5"); + const [tags, setTags] = createSignal([]); + const [description, setDescription] = createSignal(""); + const [editorInstance, setEditorInstance] = createSignal(null); + const [size, setSize] = createSignal("3"); + const [dateString, setDateString] = createSignal(""); + + let inputRef: HTMLInputElement | undefined; + + onMount(() => { + setTimeout(() => inputRef?.focus(), 100); + // Sync initial urgency to date + onUrgencySimpleChange("5"); + }); + + const onUrgencySimpleChange = (val: any) => { + const value = typeof val === 'object' ? val.value : val; + if (!value) return; + + setUrgency(value); + const level = parseInt(value); + if (!isNaN(level)) { + const newDateIso = calculateDateFromUrgency(level); + const dateObj = new Date(newDateIso); + const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); + setDateString(localIso); + } + }; + + const onDateInputChange = (e: Event) => { + const val = (e.currentTarget as HTMLInputElement).value; + setDateString(val); + if (val) { + const level = calculateUrgencyFromDate(new Date(val).toISOString()); + setUrgency(level.toString()); + } + }; + + const handleCreateTask = async () => { + let text = input(); + if (!text || !priority() || !urgency()) return; + + let finalPriority = parseInt(priority()); + let finalUrgency = parseInt(urgency()); + const finalTags = [...tags()]; + + let finalDueDate: Date | null = null; + if (dateString()) { + finalDueDate = new Date(dateString()); + } else { + const dateStr = calculateDateFromUrgency(finalUrgency); + finalDueDate = new Date(dateStr); + } + + await addTask(text.trim(), { + priority: finalPriority, + dueDate: finalDueDate || undefined, + tags: finalTags, + content: description(), + size: parseInt(size()) + }); + + toast.success("Task created successfully"); + + // Reset + setInput(""); + setPriority("5"); + setUrgency("5"); + setTags([]); + setDescription(""); + setSize("3"); + setDateString(""); + const instance = editorInstance(); + if (instance) instance.commands.setContent(""); + onUrgencySimpleChange("5"); + }; + + const applyTemplate = (template: TaskTemplate) => { + setInput(template.title); + setPriority(template.priority.toString()); + setUrgency(template.urgency.toString()); + setTags([...(template.tags || [])]); + setDescription(template.content || ""); + + const level = template.urgency; + const newDateIso = calculateDateFromUrgency(level); + const dateObj = new Date(newDateIso); + const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); + setDateString(localIso); + + const instance = editorInstance(); + if (instance) instance.commands.setContent(template.content || ""); + toast.info(`Applied template: ${template.name}`); + }; + + return ( +
+
+
+ + + setInput(e.currentTarget.value)} + onKeyDown={(e: any) => e.key === "Enter" && handleCreateTask()} + placeholder="Task title..." + class="border-none shadow-none focus-visible:ring-0 text-lg" + /> + +
+ +
+
+ + + value={priority()} + onChange={(val) => { + const v = typeof val === 'object' ? val.value : val; + if (v) setPriority(v); + }} + options={PRIORITY_OPTIONS} + optionValue="value" + optionTextValue="label" + placeholder="Priority" + itemComponent={(props) => {props.item.rawValue.label}} + > + +
+ + {PRIORITY_OPTIONS.find(o => o.value === priority())?.label || priority()} +
+
+ + +
+ +
+ + + value={size()} + onChange={(val) => { + const v = typeof val === 'object' ? val.value : val; + if (v) setSize(v); + }} + options={SIZE_OPTIONS} + optionValue="value" + optionTextValue="label" + placeholder="Size" + itemComponent={(props) => {props.item.rawValue.label}} + > + +
+ + {SIZE_OPTIONS.find(o => o.value === size())?.label || size()} +
+
+ + +
+ +
+ + + value={urgency()} + onChange={(val) => onUrgencySimpleChange(val)} + options={URGENCY_OPTIONS} + optionValue="value" + optionTextValue="label" + placeholder="Urgency" + itemComponent={(props) => {props.item.rawValue.label}} + > + +
+ + {URGENCY_OPTIONS.find(o => o.value === urgency())?.label || urgency()} +
+
+ + +
+
+ +
+
+ + + {(tag) => ( + { + const def = store.tagDefinitions.find(d => d.name === tag); + const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()); + return color ? `${color}15` : undefined; + })(), + "color": (() => { + const def = store.tagDefinitions.find(d => d.name === tag); + return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit"; + })() + }} + onClick={() => setTags(tags().filter(t => t !== tag))} + > + {tag} + + )} + +
+
+ + +
+
+ +
+ }> + + +
+
+ +
+ 0}> + + + + Templates + + + +
+ + {(template) => ( + + )} + +
+
+
+
+ +
+
+ ); +}; diff --git a/test-embed.html b/test-embed.html new file mode 100644 index 0000000..5e5a2ca --- /dev/null +++ b/test-embed.html @@ -0,0 +1,216 @@ + + + + + + + TasGrid Parent App Simulator + + + + + +

TasGrid Parent App Simulator

+ +
+
+

Login to Parent Application

+

This simulates the parent application authenticating with the shared backend.

+ +
+
+ +
+

Or send a token manually:

+ + +
+
+ +
+
+
+ Notes Embed + /embed/notes +
+ +
+ +
+
+ Quick Add Embed + /embed/quick-add +
+ +
+
+ + + + + \ No newline at end of file