adding embed routes for testing
This commit is contained in:
+45
-14
@@ -1,5 +1,7 @@
|
|||||||
import { type Component, createSignal, Show, onMount, onCleanup, lazy, Suspense } from 'solid-js';
|
import { type Component, createSignal, Show, onMount, onCleanup, lazy, Suspense } from 'solid-js';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
|
import { EmbedLayout } from './components/EmbedLayout';
|
||||||
|
import { EmbedAuthWrapper } from './components/EmbedAuthWrapper';
|
||||||
import { CriticalView } from './views/CriticalView';
|
import { CriticalView } from './views/CriticalView';
|
||||||
import { AuthCallback } from './components/Auth';
|
import { AuthCallback } from './components/Auth';
|
||||||
import { pb } from './lib/pocketbase';
|
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 DigInView = lazy(() => import('./views/DigInView').then(m => ({ default: m.DigInView })));
|
||||||
const ProgressView = lazy(() => import('./views/ProgressView').then(m => ({ default: m.ProgressView })));
|
const ProgressView = lazy(() => import('./views/ProgressView').then(m => ({ default: m.ProgressView })));
|
||||||
const HelpView = lazy(() => import('./views/HelpView').then(m => ({ default: m.HelpView })));
|
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 = () => {
|
const App: Component = () => {
|
||||||
// Basic routing state
|
// 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 (
|
return (
|
||||||
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
<Show
|
||||||
<Layout currentView={currentView()} setView={setCurrentView}>
|
when={isEmbed()}
|
||||||
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
fallback={
|
||||||
<Show when={currentView() === "critical"}><CriticalView /></Show>
|
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
||||||
<Show when={currentView() === "urgency"}><UrgencyView /></Show>
|
<Layout currentView={currentView()} setView={setCurrentView}>
|
||||||
<Show when={currentView() === "priority"}><PriorityView /></Show>
|
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
||||||
<Show when={currentView() === "matrix"}><MatrixView /></Show>
|
<Show when={currentView() === "critical"}><CriticalView /></Show>
|
||||||
<Show when={currentView() === "snowball"}><SnowballView /></Show>
|
<Show when={currentView() === "urgency"}><UrgencyView /></Show>
|
||||||
<Show when={currentView() === "dig_in"}><DigInView /></Show>
|
<Show when={currentView() === "priority"}><PriorityView /></Show>
|
||||||
<Show when={currentView() === "progress"}><ProgressView /></Show>
|
<Show when={currentView() === "matrix"}><MatrixView /></Show>
|
||||||
<Show when={currentView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
<Show when={currentView() === "snowball"}><SnowballView /></Show>
|
||||||
<Show when={currentView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
<Show when={currentView() === "dig_in"}><DigInView /></Show>
|
||||||
</Suspense>
|
<Show when={currentView() === "progress"}><ProgressView /></Show>
|
||||||
</Layout>
|
<Show when={currentView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
||||||
|
<Show when={currentView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
||||||
|
</Suspense>
|
||||||
|
</Layout>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EmbedAuthWrapper>
|
||||||
|
<EmbedLayout>
|
||||||
|
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
||||||
|
<Show when={embedView() === "embed_notes"}>
|
||||||
|
<EmbedNotesView />
|
||||||
|
</Show>
|
||||||
|
<Show when={embedView() === "embed_quick_add"}>
|
||||||
|
<EmbedQuickAddView />
|
||||||
|
</Show>
|
||||||
|
</Suspense>
|
||||||
|
</EmbedLayout>
|
||||||
|
</EmbedAuthWrapper>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<EmbedAuthWrapperProps> = (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 (
|
||||||
|
<Show
|
||||||
|
when={isHydrated()}
|
||||||
|
fallback={
|
||||||
|
<div class="flex flex-col items-center justify-center h-full space-y-4">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
<p class="text-sm text-muted-foreground">Waiting for authentication...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { type Component, type JSX } from "solid-js";
|
||||||
|
|
||||||
|
interface EmbedLayoutProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmbedLayout: Component<EmbedLayoutProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
||||||
|
<main class="flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth bg-background">
|
||||||
|
<div class="w-full h-full mx-auto p-4">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { type Component, createSignal } from "solid-js";
|
||||||
|
import { NotepadView } from "./NotepadView";
|
||||||
|
|
||||||
|
export const EmbedNotesView: Component = () => {
|
||||||
|
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-full w-full">
|
||||||
|
<NotepadView
|
||||||
|
selectedNoteId={selectedNoteId()}
|
||||||
|
setSelectedNoteId={setSelectedNoteId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<string>("5");
|
||||||
|
const [urgency, setUrgency] = createSignal<string>("5");
|
||||||
|
const [tags, setTags] = createSignal<string[]>([]);
|
||||||
|
const [description, setDescription] = createSignal("");
|
||||||
|
const [editorInstance, setEditorInstance] = createSignal<any>(null);
|
||||||
|
const [size, setSize] = createSignal<string>("3");
|
||||||
|
const [dateString, setDateString] = createSignal<string>("");
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div class="w-full max-w-xl mx-auto bg-card border border-border rounded-xl shadow-lg overflow-hidden flex flex-col">
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="flex items-center px-4 py-3 border-b border-border sticky top-0 bg-card z-10">
|
||||||
|
<Search class="text-muted-foreground mr-3" size={20} />
|
||||||
|
<TextField class="flex-1">
|
||||||
|
<TextFieldInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={input()}
|
||||||
|
onInput={(e: any) => 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"
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 p-4 gap-4 border-b border-border bg-muted/5">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label>
|
||||||
|
<Select<any>
|
||||||
|
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) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
|
||||||
|
<div class="flex items-center gap-2 truncate">
|
||||||
|
<ArrowUpCircle size={14} class="text-primary shrink-0" />
|
||||||
|
<span class="text-sm font-medium truncate">{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || priority()}</span>
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent />
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Size</label>
|
||||||
|
<Select<any>
|
||||||
|
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) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
|
||||||
|
<div class="flex items-center gap-2 truncate">
|
||||||
|
<Gauge size={14} class="text-primary shrink-0" />
|
||||||
|
<span class="text-sm font-medium truncate">{SIZE_OPTIONS.find(o => o.value === size())?.label || size()}</span>
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent />
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Urgency</label>
|
||||||
|
<Select<any>
|
||||||
|
value={urgency()}
|
||||||
|
onChange={(val) => onUrgencySimpleChange(val)}
|
||||||
|
options={URGENCY_OPTIONS}
|
||||||
|
optionValue="value"
|
||||||
|
optionTextValue="label"
|
||||||
|
placeholder="Urgency"
|
||||||
|
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
|
||||||
|
<div class="flex items-center gap-2 truncate">
|
||||||
|
<Clock size={14} class="text-primary shrink-0" />
|
||||||
|
<span class="text-sm font-medium truncate">{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || urgency()}</span>
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent />
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center px-4 py-3 gap-4 bg-muted/5 border-b border-border">
|
||||||
|
<div class="flex-1 flex flex-wrap gap-2 items-center">
|
||||||
|
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
||||||
|
<For each={tags()}>
|
||||||
|
{(tag) => (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="h-7 px-2 text-xs gap-1 bg-background border border-border/50 cursor-pointer hover:bg-destructive/10 group/tag transition-all"
|
||||||
|
style={{
|
||||||
|
"background-color": (() => {
|
||||||
|
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}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-auto sm:min-w-[200px] space-y-1.5">
|
||||||
|
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={dateString()}
|
||||||
|
onInput={onDateInputChange}
|
||||||
|
class="w-full h-10 px-3 bg-background border border-border/50 rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 py-3 bg-card min-h-[120px]">
|
||||||
|
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||||
|
<TaskEditor
|
||||||
|
content={description()}
|
||||||
|
onUpdate={setDescription}
|
||||||
|
onEditorReady={setEditorInstance}
|
||||||
|
class="min-h-[100px] prose-sm"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 bg-muted/10 flex justify-between items-center border-t border-border shrink-0">
|
||||||
|
<Show when={(store.templates || []).length > 0}>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as={Button} variant="ghost" size="sm" class="h-9 px-3 gap-2">
|
||||||
|
<Copy size={14} />
|
||||||
|
<span class="text-[0.625rem] font-bold uppercase tracking-wider">Templates</span>
|
||||||
|
<ChevronDown size={12} class="opacity-50" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-64 p-2 bg-card border-border shadow-xl">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<For each={store.templates}>
|
||||||
|
{(template) => (
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-muted transition-colors text-left"
|
||||||
|
onClick={() => applyTemplate(template)}
|
||||||
|
>
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||||
|
<Copy size={14} />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-bold truncate">{template.name}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</Show>
|
||||||
|
<Button size="sm" onClick={handleCreateTask} disabled={!input()}>
|
||||||
|
Create Task
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TasGrid Parent App Simulator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f4f4f5;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-container {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form button {
|
||||||
|
padding: 10px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.send-manual {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.manual-token {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>TasGrid Parent App Simulator</h1>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div id="login-section">
|
||||||
|
<h3>Login to Parent Application</h3>
|
||||||
|
<p>This simulates the parent application authenticating with the shared backend.</p>
|
||||||
|
<div class="login-form">
|
||||||
|
<input type="email" id="email" placeholder="Email" value="">
|
||||||
|
<input type="password" id="password" placeholder="Password" value="">
|
||||||
|
<button onclick="login()">Login & Sync Iframes</button>
|
||||||
|
</div>
|
||||||
|
<div id="status" class="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="manual-section" style="margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px;">
|
||||||
|
<p style="font-size: 13px; color: #666;">Or send a token manually:</p>
|
||||||
|
<input type="text" id="token" class="manual-token" placeholder="Paste PB Token here...">
|
||||||
|
<button class="send-manual" onclick="sendAuthFromInput()">Send Manual Token</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="iframe-container">
|
||||||
|
<div
|
||||||
|
style="padding: 10px; background: #eee; border-bottom: 1px solid #ddd; font-weight: bold; font-size: 12px; display: flex; justify-content: space-between;">
|
||||||
|
<span>Notes Embed</span>
|
||||||
|
<code style="font-size: 10px;">/embed/notes</code>
|
||||||
|
</div>
|
||||||
|
<iframe id="notes-iframe" src="http://localhost:5173/embed/notes"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="iframe-container">
|
||||||
|
<div
|
||||||
|
style="padding: 10px; background: #eee; border-bottom: 1px solid #ddd; font-weight: bold; font-size: 12px; display: flex; justify-content: space-between;">
|
||||||
|
<span>Quick Add Embed</span>
|
||||||
|
<code style="font-size: 10px;">/embed/quick-add</code>
|
||||||
|
</div>
|
||||||
|
<iframe id="quick-add-iframe" src="http://localhost:5173/embed/quick-add"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const email = document.getElementById('email').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
statusEl.className = 'status';
|
||||||
|
statusEl.textContent = 'Logging in...';
|
||||||
|
statusEl.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||||
|
|
||||||
|
statusEl.className = 'status success';
|
||||||
|
statusEl.textContent = `Logged in as ${authData.record.email}! Syncing iframes...`;
|
||||||
|
|
||||||
|
// Automatically send auth to iframes
|
||||||
|
sendAuth(pb.authStore.token, authData.record);
|
||||||
|
} catch (err) {
|
||||||
|
statusEl.className = 'status error';
|
||||||
|
statusEl.textContent = 'Login failed: ' + err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAuthFromInput() {
|
||||||
|
const token = document.getElementById('token').value;
|
||||||
|
if (!token) {
|
||||||
|
alert("Please enter a token first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendAuth(token, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAuth(token, user) {
|
||||||
|
const payload = {
|
||||||
|
type: "TASGRID_AUTH",
|
||||||
|
token: token,
|
||||||
|
user: user
|
||||||
|
};
|
||||||
|
|
||||||
|
const notesIframe = document.getElementById('notes-iframe').contentWindow;
|
||||||
|
const quickAddIframe = document.getElementById('quick-add-iframe').contentWindow;
|
||||||
|
|
||||||
|
console.log("Sending auth to iframes...", payload);
|
||||||
|
notesIframe.postMessage(payload, "*");
|
||||||
|
quickAddIframe.postMessage(payload, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle case where user refreshed but is still logged in to the parent
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
if (pb.authStore.isValid && pb.authStore.model) {
|
||||||
|
document.getElementById('email').value = pb.authStore.model.email || "";
|
||||||
|
statusEl.className = 'status success';
|
||||||
|
statusEl.textContent = 'Session restored. Click login or send manual token to sync.';
|
||||||
|
statusEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user