Skip to content

Commit 059a3d0

Browse files
templates
1 parent 0481f56 commit 059a3d0

File tree

6 files changed

+217
-137
lines changed

6 files changed

+217
-137
lines changed

apps/desktop/src/components/settings/template/editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function TemplateEditor({ id }: { id: string }) {
6868
value={field.state.value}
6969
onChange={(e) => field.handleChange(e.target.value)}
7070
placeholder="Describe the template purpose and usage..."
71-
className="min-h-[100px] resize-none"
71+
className="min-h-[100px] resize-none shadow-none"
7272
/>
7373
</div>
7474
)}
Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { useState } from "react";
1+
import { useQuery } from "@tanstack/react-query";
2+
import { BookText } from "lucide-react";
3+
import { useMemo, useState } from "react";
4+
import { createQueries } from "tinybase/with-schemas";
25

6+
import * as main from "../../../store/tinybase/main";
37
import { TemplateEditor } from "./editor";
4-
import { LocalTemplates } from "./local";
5-
import { RemoteTemplates } from "./remote";
68
import { TemplateSearch } from "./search";
7-
import { useTemplateNavigation } from "./utils";
9+
import { TemplateCard } from "./shared";
10+
import { normalizeTemplateWithId, useTemplateNavigation } from "./utils";
811

912
export function SettingsTemplates() {
1013
const { templateId } = useTemplateNavigation();
@@ -18,12 +21,133 @@ export function SettingsTemplates() {
1821

1922
function TemplateList() {
2023
const [searchQuery, setSearchQuery] = useState("");
24+
const userTemplates = useUserTemplates();
25+
const { data: suggestedTemplates = [] } = useSuggestedTemplates(searchQuery);
26+
const { goToEdit, cloneAndEdit } = useTemplateNavigation();
27+
28+
const hasNoResults =
29+
userTemplates.length === 0 && suggestedTemplates.length === 0;
2130

2231
return (
23-
<div className="flex flex-col gap-8">
24-
<TemplateSearch value={searchQuery} onChange={setSearchQuery} />
25-
<LocalTemplates query={searchQuery} />
26-
<RemoteTemplates query={searchQuery} />
32+
<div className="space-y-6">
33+
<div>
34+
<p className="text-xs text-neutral-600 mb-4">
35+
Create templates to structure and standardize your meeting notes
36+
</p>
37+
<div className="rounded-xl border border-neutral-200 bg-stone-50 mb-6">
38+
<TemplateSearch value={searchQuery} onChange={setSearchQuery} />
39+
</div>
40+
41+
{hasNoResults ? (
42+
<div className="text-center py-12 text-neutral-500 bg-neutral-50 rounded-lg p-4 border border-neutral-200">
43+
<BookText size={48} className="mx-auto mb-4 text-neutral-300" />
44+
<p className="text-sm">
45+
{searchQuery.length > 0
46+
? "No templates found"
47+
: "No templates yet"}
48+
</p>
49+
<p className="text-xs text-neutral-400 mt-1">
50+
{searchQuery.length > 0
51+
? "Try a different search term"
52+
: "Create a template to get started."}
53+
</p>
54+
</div>
55+
) : (
56+
<div className="space-y-4">
57+
{userTemplates.map((template) => (
58+
<TemplateCard
59+
key={template.id}
60+
title={template.title}
61+
description={template.description}
62+
category="mine"
63+
targets={template.targets}
64+
onClick={() => goToEdit(template.id)}
65+
/>
66+
))}
67+
{suggestedTemplates.map((template, index) => (
68+
<TemplateCard
69+
key={`suggested-${index}`}
70+
title={template.title}
71+
description={template.description}
72+
category={template.category}
73+
targets={template.targets}
74+
onClick={() =>
75+
cloneAndEdit({
76+
title: template.title,
77+
description: template.description,
78+
sections: template.sections,
79+
})
80+
}
81+
/>
82+
))}
83+
</div>
84+
)}
85+
</div>
2786
</div>
2887
);
2988
}
89+
90+
function useUserTemplates(): Array<main.Template & { id: string }> {
91+
const { user_id } = main.UI.useValues(main.STORE_ID);
92+
const store = main.UI.useStore(main.STORE_ID);
93+
94+
const USER_TEMPLATE_QUERY = "user_templates";
95+
96+
const queries = main.UI.useCreateQueries(
97+
store,
98+
(store) =>
99+
createQueries(store).setQueryDefinition(
100+
USER_TEMPLATE_QUERY,
101+
"templates",
102+
({ select, where }) => {
103+
select("title");
104+
select("description");
105+
select("sections");
106+
select("created_at");
107+
select("user_id");
108+
where("user_id", user_id ?? "");
109+
},
110+
),
111+
[user_id],
112+
);
113+
114+
const templates = main.UI.useResultTable(USER_TEMPLATE_QUERY, queries);
115+
116+
return useMemo(() => {
117+
return Object.entries(templates as Record<string, unknown>).map(
118+
([id, template]) => normalizeTemplateWithId(id, template),
119+
);
120+
}, [templates]);
121+
}
122+
123+
function useSuggestedTemplates(query: string) {
124+
return useQuery({
125+
queryKey: ["settings", "templates", "suggestions"],
126+
queryFn: async () => {
127+
const response = await fetch("https://hyprnote.com/api/templates", {
128+
headers: { Accept: "application/json" },
129+
});
130+
const data: main.Template[] = await response.json();
131+
return data;
132+
},
133+
select: (data) => {
134+
if (!query) {
135+
return data;
136+
}
137+
138+
const lowerQuery = query.toLowerCase();
139+
140+
return data.filter((template) => {
141+
const titleMatch = template.title.toLowerCase().includes(lowerQuery);
142+
const categoryMatch = template.category
143+
?.toLowerCase()
144+
.includes(lowerQuery);
145+
const targetsMatch = template.targets?.some((target) =>
146+
target?.toLowerCase().includes(lowerQuery),
147+
);
148+
149+
return titleMatch || categoryMatch || targetsMatch;
150+
});
151+
},
152+
});
153+
}
Lines changed: 9 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import { useForm } from "@tanstack/react-form";
21
import { Search } from "lucide-react";
3-
import { useEffect } from "react";
4-
5-
import { Input } from "@hypr/ui/components/ui/input";
62

73
export function TemplateSearch({
84
value,
@@ -11,52 +7,16 @@ export function TemplateSearch({
117
value: string;
128
onChange: (value: string) => void;
139
}) {
14-
const form = useForm({
15-
defaultValues: {
16-
query: value,
17-
},
18-
onSubmit: ({ value: submitted }) => {
19-
onChange(submitted.query);
20-
},
21-
});
22-
23-
useEffect(() => {
24-
const current = form.getFieldValue("query");
25-
if (current !== value) {
26-
form.setFieldValue("query", value);
27-
}
28-
}, [form, value]);
29-
3010
return (
31-
<form
32-
onSubmit={(event) => {
33-
event.preventDefault();
34-
form.handleSubmit();
35-
}}
36-
className="relative"
37-
>
38-
<Search
39-
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400"
40-
size={16}
11+
<div className="flex items-center gap-2 pl-4 pr-2 h-12">
12+
<Search className="size-4 text-neutral-400" />
13+
<input
14+
type="text"
15+
value={value}
16+
onChange={(e) => onChange(e.target.value)}
17+
placeholder="Search templates..."
18+
className="flex-1 text-sm text-neutral-900 placeholder:text-neutral-500 focus:outline-none bg-transparent"
4119
/>
42-
<form.Field name="query">
43-
{(field) => (
44-
<Input
45-
type="text"
46-
placeholder="Search templates..."
47-
value={field.state.value}
48-
onChange={(event) => {
49-
const nextValue = event.target.value;
50-
field.handleChange(nextValue);
51-
if (nextValue !== value) {
52-
onChange(nextValue);
53-
}
54-
}}
55-
onBlur={field.handleBlur}
56-
className="pl-9 shadow-none"
57-
/>
58-
)}
59-
</form.Field>
60-
</form>
20+
</div>
6121
);
6222
}

apps/desktop/src/components/settings/template/sections.tsx

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -141,34 +141,32 @@ export function SectionsList({
141141

142142
return (
143143
<div className="flex flex-col space-y-3">
144-
<div className="bg-neutral-50 rounded-lg p-4">
145-
<Reorder.Group values={drafts} onReorder={reorderSections}>
146-
<div className="flex flex-col space-y-2">
147-
{drafts.map((draft) => (
148-
<Reorder.Item key={draft.key} value={draft}>
149-
<SectionItem
150-
disabled={disabled}
151-
item={draft}
152-
onChange={changeSection}
153-
onDelete={deleteSection}
154-
dragControls={controls}
155-
/>
156-
</Reorder.Item>
157-
))}
158-
</div>
159-
</Reorder.Group>
160-
161-
<Button
162-
variant="outline"
163-
size="sm"
164-
className="mt-2 text-sm w-full"
165-
onClick={addSection}
166-
disabled={disabled}
167-
>
168-
<Plus className="mr-2 h-4 w-4" />
169-
Add Section
170-
</Button>
171-
</div>
144+
<Reorder.Group values={drafts} onReorder={reorderSections}>
145+
<div className="flex flex-col space-y-2">
146+
{drafts.map((draft) => (
147+
<Reorder.Item key={draft.key} value={draft}>
148+
<SectionItem
149+
disabled={disabled}
150+
item={draft}
151+
onChange={changeSection}
152+
onDelete={deleteSection}
153+
dragControls={controls}
154+
/>
155+
</Reorder.Item>
156+
))}
157+
</div>
158+
</Reorder.Group>
159+
160+
<Button
161+
variant="outline"
162+
size="sm"
163+
className="text-sm w-full"
164+
onClick={addSection}
165+
disabled={disabled}
166+
>
167+
<Plus className="mr-2 h-4 w-4" />
168+
Add Section
169+
</Button>
172170
</div>
173171
);
174172
}
@@ -189,16 +187,9 @@ function SectionItem({
189187
const [isFocused, setIsFocused] = useState(false);
190188

191189
return (
192-
<div
193-
className={cn([
194-
"group relative rounded-lg border p-3 transition-all bg-white",
195-
isFocused
196-
? "border-blue-500"
197-
: "border-border hover:border-neutral-300",
198-
])}
199-
>
190+
<div className="group relative bg-white">
200191
<button
201-
className="absolute left-2 top-2 cursor-move opacity-0 group-hover:opacity-30 hover:opacity-60 transition-opacity"
192+
className="absolute -left-5 top-2.5 cursor-move opacity-0 group-hover:opacity-30 hover:opacity-60 transition-opacity"
202193
onPointerDown={(event) => dragControls.start(event)}
203194
disabled={disabled}
204195
>
@@ -213,18 +204,25 @@ function SectionItem({
213204
<X size={16} />
214205
</button>
215206

216-
<div className="ml-5 mr-5 space-y-1">
207+
<div className="space-y-1">
217208
<Input
218209
disabled={disabled}
219210
value={item.title}
220211
onChange={(e) => onChange({ ...item, title: e.target.value })}
221-
onFocus={() => setIsFocused(true)}
222-
onBlur={() => setIsFocused(false)}
223212
placeholder="Untitled"
224-
className="border-0 bg-transparent p-0 text-lg font-medium shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
213+
className="border-0 bg-transparent p-0 font-medium shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
225214
/>
226215

227-
<div className="min-h-[100px] border rounded-md">
216+
<div
217+
className={cn(
218+
"min-h-[100px] border rounded-md overflow-clip transition-colors",
219+
isFocused
220+
? "border-blue-500 ring-2 ring-primary/20"
221+
: "border-input",
222+
)}
223+
onFocus={() => setIsFocused(true)}
224+
onBlur={() => setIsFocused(false)}
225+
>
228226
<CodeMirrorEditor
229227
value={item.description}
230228
onChange={(value) => onChange({ ...item, description: value })}

0 commit comments

Comments
 (0)