From 74d6bacec74b6abecc40ea6bc65a3636d9e87529 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 10 Oct 2025 11:04:38 +0900 Subject: [PATCH 1/6] scaffold --- .../src/components/main/body/contacts.tsx | 23 ++++++++++++++ .../src/components/main/body/index.tsx | 30 ++++++++++--------- .../components/main/sidebar/profile/index.tsx | 4 +-- apps/desktop2/src/store/zustand/tabs.ts | 14 ++++++--- 4 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 apps/desktop2/src/components/main/body/contacts.tsx diff --git a/apps/desktop2/src/components/main/body/contacts.tsx b/apps/desktop2/src/components/main/body/contacts.tsx new file mode 100644 index 000000000..e13d3faba --- /dev/null +++ b/apps/desktop2/src/components/main/body/contacts.tsx @@ -0,0 +1,23 @@ +import { Contact2Icon } from "lucide-react"; + +import { type Tab } from "../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "./shared"; + +export const TabItemContact: TabItem = ({ tab, handleClose, handleSelect }) => { + return ( + } + title={"Contacts"} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +export function TabContentContact({ tab }: { tab: Tab }) { + if (tab.type !== "contacts") { + return null; + } + return null; +} diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index e6cec8333..a315fd1b6 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -1,11 +1,12 @@ import clsx from "clsx"; +import { PanelLeftOpenIcon } from "lucide-react"; import { Reorder } from "motion/react"; import { type Tab, uniqueIdfromTab, useTabs } from "../../../store/zustand/tabs"; import { useLeftSidebar } from "@hypr/utils/contexts"; -import { PanelLeftOpenIcon } from "lucide-react"; import { TabContentCalendar, TabItemCalendar } from "./calendars"; +import { TabContentContact, TabItemContact } from "./contacts"; import { TabContentEvent, TabItemEvent } from "./events"; import { TabContentFolder, TabItemFolder } from "./folders"; import { TabContentHuman, TabItemHuman } from "./humans"; @@ -81,22 +82,23 @@ function TabItem( if (tab.type === "sessions") { return ; } - if (tab.type === "events") { return ; } - - if (tab.type === "calendars") { - return ; - } if (tab.type === "folders") { return ; } - if (tab.type === "humans") { return ; } + if (tab.type === "calendars") { + return ; + } + if (tab.type === "contacts") { + return ; + } + return null; } @@ -104,22 +106,22 @@ function Content({ tab }: { tab: Tab }) { if (tab.type === "sessions") { return ; } - if (tab.type === "events") { return ; } - - if (tab.type === "calendars") { - return ; - } - if (tab.type === "folders") { return ; } - if (tab.type === "humans") { return ; } + if (tab.type === "calendars") { + return ; + } + if (tab.type === "contacts") { + return ; + } + return null; } diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx index 1543cf049..3640d1432 100644 --- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx @@ -49,9 +49,9 @@ export function ProfileSection() { }, [openNew, closeMenu]); const handleClickContacts = useCallback(() => { - console.log("Contacts"); + openNew({ type: "contacts", active: true }); closeMenu(); - }, [closeMenu]); + }, [openNew, closeMenu]); const handleClickDailyNote = useCallback(() => { console.log("Daily note"); diff --git a/apps/desktop2/src/store/zustand/tabs.ts b/apps/desktop2/src/store/zustand/tabs.ts index e1c299bbc..f9b27d808 100644 --- a/apps/desktop2/src/store/zustand/tabs.ts +++ b/apps/desktop2/src/store/zustand/tabs.ts @@ -125,14 +125,17 @@ export const tabSchema = z.discriminatedUnion("type", [ type: z.literal("organizations" satisfies typeof TABLES[number]), id: z.string(), }), - baseTabSchema.extend({ - type: z.literal("calendars" satisfies typeof TABLES[number]), - month: z.coerce.date(), - }), baseTabSchema.extend({ type: z.literal("folders" satisfies typeof TABLES[number]), id: z.string().nullable(), }), + baseTabSchema.extend({ + type: z.literal("contacts"), + }), + baseTabSchema.extend({ + type: z.literal("calendars"), + month: z.coerce.date(), + }), ]); export type Tab = z.infer; @@ -148,6 +151,7 @@ export const rowIdfromTab = (tab: Tab): string => { case "organizations": return tab.id; case "calendars": + case "contacts": throw new Error("invalid_resource"); case "folders": if (!tab.id) { @@ -169,6 +173,8 @@ export const uniqueIdfromTab = (tab: Tab): string => { return `organizations-${tab.id}`; case "calendars": return `calendars-${tab.month.getFullYear()}-${tab.month.getMonth()}`; + case "contacts": + return `contacts`; case "folders": return `folders-${tab.id ?? "all"}`; } From f508e137cdab627565a6099a7239d1d2ec5ec147 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 10 Oct 2025 11:10:19 +0900 Subject: [PATCH 2/6] mostly structure change --- apps/desktop2/package.json | 2 + apps/desktop2/src-tauri/src/lib.rs | 1 + .../body/{contacts.tsx => contacts/index.tsx} | 9 +- .../components/main/body/contacts/temp.tsx | 1000 +++++++++++++++++ apps/desktop2/src/store/tinybase/persisted.ts | 2 + pnpm-lock.yaml | 167 ++- 6 files changed, 1130 insertions(+), 51 deletions(-) rename apps/desktop2/src/components/main/body/{contacts.tsx => contacts/index.tsx} (58%) create mode 100644 apps/desktop2/src/components/main/body/contacts/temp.tsx diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index be1792e40..0228f63a8 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -16,12 +16,14 @@ "@hypr/db": "workspace:*", "@hypr/plugin-db2": "workspace:*", "@hypr/plugin-windows": "workspace:*", + "@hypr/plugin-analytics": "workspace:*", "@hypr/tiptap": "workspace:^", "@hypr/ui": "workspace:^", "@hypr/utils": "workspace:^", "@sentry/react": "^8.55.0", "@supabase/supabase-js": "^2.74.0", "@t3-oss/env-core": "^0.13.8", + "@tanstack/react-query": "^5.90.2", "@tanstack/react-router": "^1.132.47", "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.8.0", diff --git a/apps/desktop2/src-tauri/src/lib.rs b/apps/desktop2/src-tauri/src/lib.rs index dcb46c416..4899b86cb 100644 --- a/apps/desktop2/src-tauri/src/lib.rs +++ b/apps/desktop2/src-tauri/src/lib.rs @@ -42,6 +42,7 @@ pub async fn main() { .plugin(tauri_plugin_analytics::init()) .plugin(tauri_plugin_db2::init()) .plugin(tauri_plugin_tracing::init()) + .plugin(tauri_plugin_analytics::init()) .plugin(tauri_plugin_listener::init()) .plugin(tauri_plugin_local_stt::init()) .plugin(tauri_plugin_updater::Builder::new().build()) diff --git a/apps/desktop2/src/components/main/body/contacts.tsx b/apps/desktop2/src/components/main/body/contacts/index.tsx similarity index 58% rename from apps/desktop2/src/components/main/body/contacts.tsx rename to apps/desktop2/src/components/main/body/contacts/index.tsx index e13d3faba..1842276bd 100644 --- a/apps/desktop2/src/components/main/body/contacts.tsx +++ b/apps/desktop2/src/components/main/body/contacts/index.tsx @@ -1,7 +1,8 @@ import { Contact2Icon } from "lucide-react"; -import { type Tab } from "../../../store/zustand/tabs"; -import { type TabItem, TabItemBase } from "./shared"; +import * as persisted from "../../../../store/tinybase/persisted"; +import { type Tab } from "../../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "../shared"; export const TabItemContact: TabItem = ({ tab, handleClose, handleSelect }) => { return ( @@ -19,5 +20,7 @@ export function TabContentContact({ tab }: { tab: Tab }) { if (tab.type !== "contacts") { return null; } - return null; + + const organizations = persisted.UI.useResultTable(persisted.QUERIES.visibleOrganizations); + return
{JSON.stringify(organizations, null, 2)}
; } diff --git a/apps/desktop2/src/components/main/body/contacts/temp.tsx b/apps/desktop2/src/components/main/body/contacts/temp.tsx new file mode 100644 index 000000000..cfab58474 --- /dev/null +++ b/apps/desktop2/src/components/main/body/contacts/temp.tsx @@ -0,0 +1,1000 @@ +// export function ContactView({ userId, initialPersonId, initialOrgId }: ContactViewProps) { +// // Simple state initialization - handles both normal and deep-link cases +// const [selectedOrganization, setSelectedOrganization] = useState(initialOrgId || null); +// const [selectedPerson, setSelectedPerson] = useState(initialPersonId || null); + +// const [editingPerson, setEditingPerson] = useState(null); +// const [editingOrg, setEditingOrg] = useState(null); +// const [showNewOrg, setShowNewOrg] = useState(false); +// const [sortOption, setSortOption] = useState<"alphabetical" | "oldest" | "newest">("alphabetical"); +// const queryClient = useQueryClient(); + +// // Load organizations once and keep cached (global data) +// const { data: organizations = [] } = useQuery({ +// queryKey: ["organizations"], +// queryFn: () => dbCommands.listOrganizations(null), +// }); + +// // Load user's own profile +// const { data: userProfile } = useQuery({ +// queryKey: ["user-profile", userId], +// queryFn: async () => { +// try { +// return await dbCommands.getHuman(userId); +// } catch (error) { +// console.error("Error fetching user profile:", error); +// return null; +// } +// }, +// }); + +// // Load all people once and keep cached (user-specific data) +// const { data: allPeople = [] } = useQuery({ +// queryKey: ["all-people", userId], +// queryFn: async () => { +// try { +// const allHumans = await dbCommands.listHumans(null); +// return allHumans; +// } catch (error) { +// console.error("Error fetching all people:", error); +// return []; +// } +// }, +// }); + +// // Merge user profile with all people, ensuring user's own profile is included +// const allPeopleWithUser = React.useMemo(() => { +// if (!userProfile) { +// return allPeople; +// } + +// // Check if user is already in the list +// const userInList = allPeople.some(person => person.id === userId); + +// if (userInList) { +// return allPeople; +// } else { +// // Add user profile to the beginning of the list +// return [userProfile, ...allPeople]; +// } +// }, [allPeople, userProfile, userId]); + +// // Person sessions - only runs when person is selected +// const { data: personSessions = [] } = useQuery({ +// queryKey: ["person-sessions", selectedPerson || "none"], +// queryFn: async () => { +// if (!selectedPerson) { +// throw new Error("Query should not run when selectedPerson is null"); +// } + +// const sessions = await dbCommands.listSessions({ +// type: "search", +// query: "", +// user_id: userId, +// limit: 100, +// }); + +// const sessionsWithPerson = []; +// for (const session of sessions) { +// try { +// const participants = await dbCommands.sessionListParticipants(session.id); +// if (participants.some(p => p.id === selectedPerson)) { +// sessionsWithPerson.push(session); +// } +// } catch (error) { +// console.error("Error fetching participants for session", session.id, error); +// } +// } + +// return sessionsWithPerson; +// }, +// enabled: selectedPerson !== null && selectedPerson !== undefined && selectedPerson !== "", +// gcTime: 5 * 60 * 1000, +// staleTime: 30 * 1000, +// }); + +// const isValidName = (name: string | null): boolean => { +// return name !== null && name !== "" && name !== "Null"; +// }; + +// const displayPeople = React.useMemo(() => { +// let filtered = (selectedOrganization +// ? allPeopleWithUser.filter(person => person.organization_id === selectedOrganization) +// : allPeopleWithUser).filter(person => person.id === userId || isValidName(person.full_name)); + +// if (sortOption === "alphabetical") { +// filtered = [...filtered].sort((a, b) => { +// const nameA = (a.full_name || a.email || "").toLowerCase(); +// const nameB = (b.full_name || b.email || "").toLowerCase(); +// return nameA.localeCompare(nameB); +// }); +// } else if (sortOption === "newest") { +// filtered = [...filtered].reverse(); +// } + +// return filtered; +// }, [selectedOrganization, allPeopleWithUser, userId, sortOption]); + +// const selectedPersonData = displayPeople.find(p => p.id === selectedPerson); + +// const handleSessionClick = (sessionId: string) => { +// const path = { to: "/app/note/$id", params: { id: sessionId } } as const satisfies LinkProps; + +// windowsCommands.windowShow({ type: "main" }).then(() => { +// windowsCommands.windowEmitNavigate({ type: "main" }, { +// path: path.to.replace("$id", path.params.id), +// search: null, +// }); +// }); +// }; + +// const handleEditPerson = (personId: string) => { +// setEditingPerson(personId); +// }; + +// const handleEditOrganization = (organizationId: string) => { +// setEditingOrg(organizationId); +// }; + +// const deletePersonMutation = useMutation({ +// mutationFn: (personId: string) => dbCommands.deleteHuman(personId), +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["all-people"] }); +// queryClient.invalidateQueries({ queryKey: ["user-profile"] }); + +// if (selectedPerson === selectedPersonData?.id) { +// setSelectedPerson(null); +// } +// }, +// }); + +// const handleDeletePerson = async (personId: string) => { +// const userConfirmed = await confirm( +// "Are you sure you want to delete this contact? This action cannot be undone.", +// ); +// if (userConfirmed) { +// deletePersonMutation.mutate(personId); +// } +// }; + +// return ( +//
+//
+//
+//

Organizations

+// +//
+//
+//
+// +// {showNewOrg && ( +// { +// setShowNewOrg(false); +// setSelectedOrganization(org.id); +// }} +// onCancel={() => setShowNewOrg(false)} +// /> +// )} +// {organizations.map((org) => ( +// editingOrg === org.id +// ? ( +// setEditingOrg(null)} +// onCancel={() => setEditingOrg(null)} +// /> +// ) +// : ( +//
+// +// +//
+// ) +// ))} +//
+//
+//
+ +//
+//
+//

People

+//
+// +// +//
+//
+//
+//
+// {displayPeople.map((person) => ( +// +// ))} +//
+//
+//
+ +//
+// {selectedPersonData +// ? ( +// editingPerson === selectedPersonData.id +// ? ( +// setEditingPerson(null)} +// onCancel={() => setEditingPerson(null)} +// /> +// ) +// : ( +// <> +//
+//
+//
+// +// {getInitials(selectedPersonData.full_name || selectedPersonData.email)} +// +//
+//
+//
+//
+//

+// {selectedPersonData.full_name || "Unnamed Contact"} +// {selectedPersonData.id === userId && ( +// You +// )} +//

+// {selectedPersonData.job_title && ( +//

{selectedPersonData.job_title}

+// )} +// {selectedPersonData.email && ( +//

{selectedPersonData.email}

+// )} +// {selectedPersonData.organization_id && ( +// +// )} +// {!selectedPersonData.is_user && selectedPersonData.email && ( +// +// )} +//
+//
+// +// {!selectedPersonData.is_user && ( +// +// )} +//
+//
+//
+//
+//
+ +//
+//

Related Notes

+//
+//
+// {personSessions.length > 0 +// ? ( +// personSessions.map((session) => ( +// +// )) +// ) +// :

No related notes found

} +//
+//
+//
+// +// ) +// ) +// : ( +//
+//

Select a person to view details

+//
+// )} +//
+//
+// ); +// } + +// function OrganizationInfo({ organizationId }: { organizationId: string }) { +// const { data: organization } = useQuery({ +// queryKey: ["organization", organizationId], +// queryFn: () => dbCommands.getOrganization(organizationId), +// enabled: !!organizationId, +// }); + +// if (!organization) { +// return null; +// } + +// return ( +//

+// {organization.name} +//

+// ); +// } + +// function EditPersonForm({ +// person, +// organizations, +// onSave, +// onCancel, +// }: { +// person: Human; +// organizations: Organization[]; +// onSave: () => void; +// onCancel: () => void; +// }) { +// const queryClient = useQueryClient(); +// const [formData, setFormData] = useState({ +// full_name: person.full_name || "", +// email: person.email || "", +// job_title: person.job_title || "", +// linkedin_username: person.linkedin_username || "", +// organization_id: person.organization_id, +// }); + +// const updatePersonMutation = useMutation({ +// mutationFn: (data: Partial) => +// dbCommands.upsertHuman({ +// ...person, +// ...data, +// }), +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["all-people"] }); +// queryClient.invalidateQueries({ queryKey: ["user-profile"] }); +// onSave(); +// }, +// onError: () => { +// console.error("Failed to update contact"); +// }, +// }); + +// const handleSubmit = (e: React.FormEvent) => { +// e.preventDefault(); +// updatePersonMutation.mutate(formData); +// }; + +// return ( +//
+// {/* Header */} +//
+//
+//

Edit Contact

+//
+// +// +//
+//
+//
+ +// {/* Content */} +//
+// {/* Avatar Section */} +//
+//
+// +// {getInitials(formData.full_name || "?")} +// +//
+//
+ +// {/* Form Section */} +//
+// {/* Name Field */} +//
+//
Name
+//
+// setFormData({ ...formData, full_name: e.target.value })} +// placeholder="John Doe" +// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" +// /> +//
+//
+ +// {/* Job Title Field */} +//
+//
Job Title
+//
+// setFormData({ ...formData, job_title: e.target.value })} +// placeholder="Software Engineer" +// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" +// /> +//
+//
+ +// {/* Company Field */} +//
+//
Company
+//
+// setFormData({ ...formData, organization_id: orgId })} +// /> +//
+//
+ +// {/* Email Field */} +//
+//
Email
+//
+// setFormData({ ...formData, email: e.target.value })} +// placeholder="john@example.com" +// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" +// /> +//
+//
+ +// {/* LinkedIn Field */} +//
+//
LinkedIn
+//
+// setFormData({ ...formData, linkedin_username: e.target.value })} +// placeholder="https://www.linkedin.com/in/johntopia/" +// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" +// /> +//
+//
+//
+//
+//
+// ); +// } + +// function ContactOrganizationSelector({ +// value, +// onChange, +// }: { +// value: string | null; +// onChange: (orgId: string | null) => void; +// }) { +// const [open, setOpen] = useState(false); + +// const { data: organization } = useQuery({ +// queryKey: ["org", value], +// queryFn: () => (value ? dbCommands.getOrganization(value) : null), +// enabled: !!value, +// }); + +// const handleRemoveOrganization = () => { +// onChange(null); +// }; + +// return ( +// +// +//
+// {organization +// ? ( +//
+// {organization.name} +// +// { +// e.stopPropagation(); +// handleRemoveOrganization(); +// }} +// /> +// +//
+// ) +// : Select organization} +//
+//
+ +// +// setOpen(false)} /> +// +//
+// ); +// } + +// function OrganizationControl({ +// onChange, +// closePopover, +// }: { +// onChange: (orgId: string | null) => void; +// closePopover: () => void; +// }) { +// const queryClient = useQueryClient(); +// const [searchTerm, setSearchTerm] = useState(""); + +// const addOrganizationMutation = useMutation({ +// mutationFn: async ({ name }: { name: string }) => { +// const newOrg = await dbCommands.upsertOrganization({ +// id: crypto.randomUUID(), +// name, +// description: null, +// }); + +// onChange(newOrg.id); +// return newOrg; +// }, +// onSuccess: () => { +// queryClient.invalidateQueries({ +// queryKey: ["organizations"], +// }); +// closePopover(); +// }, +// }); + +// const { data: organizations = [] } = useQuery({ +// queryKey: ["organizations", searchTerm], +// queryFn: () => { +// if (!searchTerm) { +// return dbCommands.listOrganizations(null); +// } +// return dbCommands.listOrganizations({ search: [5, searchTerm] }); +// }, +// }); + +// const handleSubmit = (e: React.SyntheticEvent) => { +// e.preventDefault(); + +// const name = searchTerm.trim(); +// if (name === "") { +// return; +// } + +// addOrganizationMutation.mutate({ name }); +// setSearchTerm(""); +// }; + +// const handleKeyDown = (e: React.KeyboardEvent) => { +// if (e.key === "Enter") { +// e.preventDefault(); + +// const name = searchTerm.trim(); +// if (name === "") { +// return; +// } + +// addOrganizationMutation.mutate({ name }); +// setSearchTerm(""); +// } +// }; + +// const selectOrganization = (orgId: string) => { +// onChange(orgId); +// closePopover(); +// }; + +// return ( +//
+//
Organization
+ +//
+//
+//
+// +// +// +// setSearchTerm(e.target.value)} +// onKeyDown={handleKeyDown} +// placeholder="Search or add company" +// className="w-full bg-transparent text-sm focus:outline-none placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0" +// /> +//
+ +// {searchTerm.trim() && ( +//
+// {organizations.map((org) => ( +// +// ))} + +// {organizations.length === 0 && ( +// +// )} +//
+// )} + +// {!searchTerm.trim() && organizations.length > 0 && ( +//
+// {organizations.map((org) => ( +// +// ))} +//
+// )} +//
+//
+//
+// ); +// } + +// function EditOrganizationForm({ +// organization, +// onSave, +// onCancel, +// }: { +// organization: Organization; +// onSave: () => void; +// onCancel: () => void; +// }) { +// const queryClient = useQueryClient(); +// const [name, setName] = useState(organization.name); +// const formRef = useRef(null); + +// const updateOrgMutation = useMutation({ +// mutationFn: (data: Partial) => +// dbCommands.upsertOrganization({ +// ...organization, +// ...data, +// }), +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ["organizations"] }); +// queryClient.invalidateQueries({ queryKey: ["organization", organization.id] }); +// onSave(); +// }, +// onError: () => { +// console.error("Failed to update organization"); +// }, +// }); + +// useEffect(() => { +// const handleClickOutside = (event: MouseEvent) => { +// if (formRef.current && !formRef.current.contains(event.target as Node)) { +// onCancel(); +// } +// }; + +// document.addEventListener("mousedown", handleClickOutside); +// return () => { +// document.removeEventListener("mousedown", handleClickOutside); +// }; +// }, [onCancel]); + +// const handleSubmit = (e: React.FormEvent) => { +// e.preventDefault(); +// if (name.trim()) { +// updateOrgMutation.mutate({ name: name.trim() }); +// } +// }; + +// const handleKeyDown = (e: React.KeyboardEvent) => { +// if (e.key === "Enter") { +// e.preventDefault(); +// if (name.trim()) { +// updateOrgMutation.mutate({ name: name.trim() }); +// } +// } +// if (e.key === "Escape") { +// onCancel(); +// } +// }; + +// return ( +//
+//
+//
+// setName(e.target.value)} +// onKeyDown={handleKeyDown} +// placeholder="Organization name" +// className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" +// autoFocus +// /> +// {name.trim() && ( +// +// )} +//
+//
+//
+// ); +// } + +// function NewOrganizationForm({ +// onSave, +// onCancel, +// }: { +// onSave: (org: Organization) => void; +// onCancel: () => void; +// }) { +// const queryClient = useQueryClient(); +// const [name, setName] = useState(""); +// const formRef = useRef(null); + +// const createOrgMutation = useMutation({ +// mutationFn: (name: string) => +// dbCommands.upsertOrganization({ +// id: crypto.randomUUID(), +// name, +// description: null, +// }), +// onSuccess: (org) => { +// queryClient.invalidateQueries({ queryKey: ["organizations"] }); +// onSave(org); +// }, +// onError: () => { +// console.error("Failed to create organization"); +// }, +// }); + +// useEffect(() => { +// const handleClickOutside = (event: MouseEvent) => { +// if (formRef.current && !formRef.current.contains(event.target as Node)) { +// onCancel(); +// } +// }; + +// document.addEventListener("mousedown", handleClickOutside); +// return () => { +// document.removeEventListener("mousedown", handleClickOutside); +// }; +// }, [onCancel]); + +// const handleSubmit = (e: React.FormEvent) => { +// e.preventDefault(); +// if (name.trim()) { +// createOrgMutation.mutate(name.trim()); +// } +// }; + +// const handleKeyDown = (e: React.KeyboardEvent) => { +// if (e.key === "Enter") { +// e.preventDefault(); +// if (name.trim()) { +// createOrgMutation.mutate(name.trim()); +// } +// } +// if (e.key === "Escape") { +// onCancel(); +// } +// }; + +// return ( +//
+//
+//
+// setName(e.target.value)} +// onKeyDown={handleKeyDown} +// placeholder="Add organization" +// className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" +// autoFocus +// /> +// {name.trim() && ( +// +// )} +//
+//
+//
+// ); +// } diff --git a/apps/desktop2/src/store/tinybase/persisted.ts b/apps/desktop2/src/store/tinybase/persisted.ts index a481d9c35..7dc04e008 100644 --- a/apps/desktop2/src/store/tinybase/persisted.ts +++ b/apps/desktop2/src/store/tinybase/persisted.ts @@ -463,6 +463,8 @@ export const StoreComponent = () => { export const QUERIES = { eventsWithoutSession: "eventsWithoutSession", sessionsWithMaybeEvent: "sessionsWithMaybeEvent", + visibleOrganizations: "visibleOrganizations", + visibleHumans: "visibleHumans", }; export const METRICS = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 024ebb361..32f5ffb4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,7 +146,7 @@ importers: version: 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-router-devtools': specifier: ^1.132.51 - version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) '@tauri-apps/api': specifier: ^2.8.0 version: 2.8.0 @@ -342,13 +342,13 @@ importers: version: 10.4.21(postcss@8.5.6) eslint: specifier: ^9.37.0 - version: 9.37.0(jiti@2.6.1) + version: 9.37.0(jiti@1.21.7) eslint-plugin-lingui: specifier: ^0.9.0 - version: 0.9.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + version: 0.9.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.37.0(jiti@2.6.1)) + version: 7.37.5(eslint@9.37.0(jiti@1.21.7)) globals: specifier: ^15.15.0 version: 15.15.0 @@ -363,7 +363,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.46.0 - version: 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) vite: specifier: ^5.4.20 version: 5.4.20(@types/node@22.18.8)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0) @@ -382,6 +382,9 @@ importers: '@hypr/db': specifier: workspace:* version: link:../../packages/db + '@hypr/plugin-analytics': + specifier: workspace:* + version: link:../../plugins/analytics '@hypr/plugin-db2': specifier: workspace:* version: link:../../plugins/db2 @@ -406,6 +409,9 @@ importers: '@t3-oss/env-core': specifier: ^0.13.8 version: 0.13.8(typescript@5.8.3)(zod@4.1.12) + '@tanstack/react-query': + specifier: ^5.90.2 + version: 5.90.2(react@19.2.0) '@tanstack/react-router': specifier: ^1.132.47 version: 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -469,10 +475,10 @@ importers: version: 10.0.0 '@tanstack/react-router-devtools': specifier: ^1.132.51 - version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) '@tanstack/router-plugin': specifier: ^1.132.51 - version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@tauri-apps/cli': specifier: ^2.8.4 version: 2.8.4 @@ -487,7 +493,7 @@ importers: version: 19.2.1(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -502,7 +508,7 @@ importers: version: 5.8.3 vite: specifier: ^7.1.9 - version: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.10.4(@types/node@24.7.0)(typescript@5.8.3))(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0) @@ -13326,9 +13332,9 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@1.21.7))': dependencies: - eslint: 9.37.0(jiti@2.6.1) + eslint: 9.37.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -15894,6 +15900,30 @@ snapshots: '@tanstack/query-core': 5.90.2 react: 19.2.0 + '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': + dependencies: + '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-devtools-core': 1.132.51(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + vite: 7.1.9(@types/node@22.18.8)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@tanstack/router-core' + - '@types/node' + - csstype + - jiti + - less + - lightningcss + - sass + - sass-embedded + - solid-js + - stylus + - sugarss + - terser + - tiny-invariant + - tsx + - yaml + '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -15918,13 +15948,13 @@ snapshots: - tsx - yaml - '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + '@tanstack/router-devtools-core': 1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@tanstack/router-core' - '@types/node' @@ -16029,6 +16059,29 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 + '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': + dependencies: + '@tanstack/router-core': 1.132.47 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.1.3) + solid-js: 1.9.7 + tiny-invariant: 1.3.3 + vite: 7.1.9(@types/node@22.18.8)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + csstype: 3.1.3 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.132.47 @@ -16052,14 +16105,14 @@ snapshots: - tsx - yaml - '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.132.47 clsx: 2.1.1 goober: 2.1.18(csstype@3.1.3) solid-js: 1.9.7 tiny-invariant: 1.3.3 - vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: csstype: 3.1.3 transitivePeerDependencies: @@ -16132,7 +16185,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/router-plugin@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) @@ -16150,7 +16203,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -16918,15 +16971,15 @@ snapshots: '@types/node': 22.18.8 optional: true - '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.0 - '@typescript-eslint/type-utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.0 - eslint: 9.37.0(jiti@2.6.1) + eslint: 9.37.0(jiti@1.21.7) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -16935,14 +16988,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.46.0 '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 9.37.0(jiti@2.6.1) + eslint: 9.37.0(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16965,13 +17018,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 9.37.0(jiti@2.6.1) + eslint: 9.37.0(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -16995,13 +17048,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.46.0 '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) - eslint: 9.37.0(jiti@2.6.1) + eslint: 9.37.0(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -17034,7 +17087,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -17042,7 +17095,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -19147,16 +19200,16 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-lingui@0.9.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-lingui@0.9.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.37.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.37.0(jiti@1.21.7) micromatch: 4.0.8 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-react@7.37.5(eslint@9.37.0(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@9.37.0(jiti@1.21.7)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -19164,7 +19217,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.37.0(jiti@2.6.1) + eslint: 9.37.0(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -19187,9 +19240,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.37.0(jiti@2.6.1): + eslint@9.37.0(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.4.0 @@ -19225,7 +19278,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.6.1 + jiti: 1.21.7 transitivePeerDependencies: - supports-color @@ -24553,13 +24606,13 @@ snapshots: typedarray@0.0.6: optional: true - typescript-eslint@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.37.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.37.0(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -24890,6 +24943,24 @@ snapshots: sugarss: 5.0.1(postcss@8.5.6) terser: 5.44.0 + vite@7.1.9(@types/node@22.18.8)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.18.8 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.1 + sugarss: 5.0.1(postcss@8.5.6) + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + vite@7.1.9(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.10 @@ -24908,7 +24979,7 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -24919,7 +24990,7 @@ snapshots: optionalDependencies: '@types/node': 24.7.0 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 lightningcss: 1.30.1 sugarss: 5.0.1(postcss@8.5.6) terser: 5.44.0 From c70bf6cb005596e40bb35fe2700db7512943b92a Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 10 Oct 2025 11:55:28 +0900 Subject: [PATCH 3/6] able to run desktop1 and desktop2 together --- apps/desktop2/src-tauri/tauri.conf.json | 2 +- apps/desktop2/vite.config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop2/src-tauri/tauri.conf.json b/apps/desktop2/src-tauri/tauri.conf.json index 00d1d7405..21238c111 100644 --- a/apps/desktop2/src-tauri/tauri.conf.json +++ b/apps/desktop2/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "identifier": "com.hyprnote2.dev", "build": { "beforeDevCommand": "pnpm -F desktop2 dev", - "devUrl": "http://localhost:1420", + "devUrl": "http://localhost:1422", "beforeBuildCommand": "pnpm -F desktop2 build", "frontendDist": "../dist" }, diff --git a/apps/desktop2/vite.config.ts b/apps/desktop2/vite.config.ts index c3a7840b4..81476b129 100644 --- a/apps/desktop2/vite.config.ts +++ b/apps/desktop2/vite.config.ts @@ -24,14 +24,14 @@ export default defineConfig(async () => ({ const tauri: UserConfig = { clearScreen: false, server: { - port: 1420, + port: 1422, strictPort: true, host: host || false, hmr: host ? { protocol: "ws", host, - port: 1421, + port: 1423, } : undefined, watch: { From f088cc6cad62f904b8d9ae8ab4ff1c3ba2e19b14 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 10 Oct 2025 12:41:53 +0900 Subject: [PATCH 4/6] made progress --- .../components/main/body/contacts/index.tsx | 990 +++++++++++++++- .../components/main/body/contacts/temp.tsx | 1000 ----------------- .../src/components/main/body/shared.tsx | 6 +- .../main/sidebar/profile/banner.tsx | 40 +- apps/desktop2/src/store/seed.ts | 46 +- apps/desktop2/src/store/tinybase/persisted.ts | 31 +- apps/desktop2/src/store/zustand/tabs.ts | 65 +- packages/db/src/schema.ts | 3 + 8 files changed, 1131 insertions(+), 1050 deletions(-) delete mode 100644 apps/desktop2/src/components/main/body/contacts/temp.tsx diff --git a/apps/desktop2/src/components/main/body/contacts/index.tsx b/apps/desktop2/src/components/main/body/contacts/index.tsx index 1842276bd..6f26b3873 100644 --- a/apps/desktop2/src/components/main/body/contacts/index.tsx +++ b/apps/desktop2/src/components/main/body/contacts/index.tsx @@ -1,7 +1,25 @@ -import { Contact2Icon } from "lucide-react"; +import { + Building2, + CircleMinus, + Contact2Icon, + CornerDownLeft, + FileText, + Pencil, + Plus, + SearchIcon, + TrashIcon, + User, + UserPlus, +} from "lucide-react"; +import React, { useState } from "react"; +import { Button } from "@hypr/ui/components/ui/button"; +import { Input } from "@hypr/ui/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; +import { cn } from "@hypr/ui/lib/utils"; import * as persisted from "../../../../store/tinybase/persisted"; -import { type Tab } from "../../../../store/zustand/tabs"; +import { type Tab, useTabs } from "../../../../store/zustand/tabs"; import { type TabItem, TabItemBase } from "../shared"; export const TabItemContact: TabItem = ({ tab, handleClose, handleSelect }) => { @@ -21,6 +39,970 @@ export function TabContentContact({ tab }: { tab: Tab }) { return null; } - const organizations = persisted.UI.useResultTable(persisted.QUERIES.visibleOrganizations); - return
{JSON.stringify(organizations, null, 2)}
; + return ( +
+ +
+ ); +} + +function ContactView({ tab }: { tab: Tab }) { + if (tab.type !== "contacts") { + return null; + } + + const updateContactsTabState = useTabs((state) => state.updateContactsTabState); + + const { selectedOrganization, selectedPerson, editingPerson, editingOrg, showNewOrg, sortOption } = tab.state; + + const setSelectedOrganization = (value: string | null) => { + updateContactsTabState(tab, { ...tab.state, selectedOrganization: value }); + }; + + const setSelectedPerson = (value: string | null) => { + updateContactsTabState(tab, { ...tab.state, selectedPerson: value }); + }; + + const setEditingPerson = (value: string | null) => { + updateContactsTabState(tab, { ...tab.state, editingPerson: value }); + }; + + const setEditingOrg = (value: string | null) => { + updateContactsTabState(tab, { ...tab.state, editingOrg: value }); + }; + + const setShowNewOrg = (value: boolean) => { + updateContactsTabState(tab, { ...tab.state, showNewOrg: value }); + }; + + const setSortOption = (value: "alphabetical" | "oldest" | "newest") => { + updateContactsTabState(tab, { ...tab.state, sortOption: value }); + }; + + const organizationsData = persisted.UI.useResultTable(persisted.QUERIES.visibleOrganizations, persisted.STORE_ID); + const humansData = persisted.UI.useResultTable(persisted.QUERIES.visibleHumans, persisted.STORE_ID); + const selectedPersonData = persisted.UI.useRow("humans", selectedPerson ?? "", persisted.STORE_ID); + + // Get humans by organization if one is selected + const humanIdsByOrg = persisted.UI.useSliceRowIds( + persisted.INDEXES.humansByOrg, + selectedOrganization ?? "", + persisted.STORE_ID, + ); + + // Convert to arrays for rendering + const organizations = Object.entries(organizationsData).map(([id, data]) => ({ + id, + ...(data as any), + })); + + const allHumans = Object.entries(humansData).map(([id, data]) => ({ + id, + ...(data as any), + })); + + // Filter humans by organization if selected + const displayPeople = selectedOrganization + ? allHumans.filter(h => humanIdsByOrg.includes(h.id)) + : allHumans; + + // Sort people based on selected option + const sortedPeople = [...displayPeople].sort((a: any, b: any) => { + if (sortOption === "alphabetical") { + return (a.name || a.email || "").localeCompare(b.name || b.email || ""); + } else if (sortOption === "newest") { + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + } else { + return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); + } + }); + + // Get sessions - for now just an empty array, we'll implement this later + const personSessions: any[] = []; + + const handleSessionClick = (_sessionId: string) => { + // Handle session click + }; + + const handleEditPerson = (personId: string) => { + setEditingPerson(personId); + }; + + const handleEditOrganization = (organizationId: string) => { + setEditingOrg(organizationId); + }; + + const handleDeletePerson = async (_personId: string) => { + // Handle delete person + }; + + const getInitials = (name: string | null) => { + if (!name) { + return "?"; + } + return name + .split(" ") + .map(n => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + return ( +
+ + + + + +
+ ); +} + +function OrganizationsColumn({ + selectedOrganization, + setSelectedOrganization, + showNewOrg, + setShowNewOrg, + editingOrg, + setEditingOrg, + organizations, + handleEditOrganization, +}: { + selectedOrganization: string | null; + setSelectedOrganization: (id: string | null) => void; + showNewOrg: boolean; + setShowNewOrg: (show: boolean) => void; + editingOrg: string | null; + setEditingOrg: (id: string | null) => void; + organizations: any[]; + handleEditOrganization: (id: string) => void; +}) { + return ( +
+
+

Organizations

+ +
+
+
+ + {showNewOrg && ( + setShowNewOrg(false)} + onCancel={() => setShowNewOrg(false)} + /> + )} + {organizations.map((org: any) => + editingOrg === org.id + ? ( + setEditingOrg(null)} + onCancel={() => setEditingOrg(null)} + /> + ) + : ( +
+ + +
+ ) + )} +
+
+
+ ); +} + +function PeopleColumn({ + displayPeople, + selectedPerson, + setSelectedPerson, + sortOption, + setSortOption, + getInitials, +}: { + displayPeople: any[]; + selectedPerson: string | null; + setSelectedPerson: (id: string | null) => void; + sortOption: "alphabetical" | "oldest" | "newest"; + setSortOption: (option: "alphabetical" | "oldest" | "newest") => void; + getInitials: (name: string | null) => string; +}) { + return ( +
+
+

People

+
+ + +
+
+
+
+ {displayPeople.map((person: any) => ( + + ))} +
+
+
+ ); +} + +function DetailsColumn({ + selectedPersonData, + editingPerson, + setEditingPerson, + organizations, + personSessions, + handleEditPerson, + handleDeletePerson, + handleSessionClick, + getInitials, +}: { + selectedPersonData: any; + editingPerson: string | null; + setEditingPerson: (id: string | null) => void; + organizations: any[]; + personSessions: any[]; + handleEditPerson: (id: string) => void; + handleDeletePerson: (id: string) => void; + handleSessionClick: (id: string) => void; + getInitials: (name: string | null) => string; +}) { + return ( +
+ {selectedPersonData + ? ( + editingPerson === selectedPersonData.id + ? ( + setEditingPerson(null)} + onCancel={() => setEditingPerson(null)} + /> + ) + : ( + <> +
+
+
+ + {getInitials(selectedPersonData.name || selectedPersonData.email)} + +
+
+
+
+

+ {selectedPersonData.name || "Unnamed Contact"} + {selectedPersonData.is_user && ( + You + )} +

+ {selectedPersonData.job_title && ( +

{selectedPersonData.job_title}

+ )} + {selectedPersonData.email && ( +

{selectedPersonData.email}

+ )} + {selectedPersonData.org_id && } + {!selectedPersonData.is_user && selectedPersonData.email && ( + + )} +
+
+ + {!selectedPersonData.is_user && ( + + )} +
+
+
+
+
+ +
+

Related Notes

+
+
+ {personSessions.length > 0 + ? ( + personSessions.map((session: any) => ( + + )) + ) + :

No related notes found

} +
+
+
+ + ) + ) + : ( +
+

Select a person to view details

+
+ )} +
+ ); +} + +function OrganizationInfo({ organizationId }: { organizationId: string }) { + const organization = persisted.UI.useRow("organizations", organizationId, persisted.STORE_ID); + + if (!organization) { + return null; + } + + return ( +

+ {organization.name} +

+ ); +} + +function EditPersonForm({ + person, + organizations: _organizations, + onSave, + onCancel, +}: { + person: any; + organizations: any[]; + onSave: () => void; + onCancel: () => void; +}) { + const personData = persisted.UI.useRow("humans", person.id, persisted.STORE_ID); + + const getInitials = (name: string | null) => { + if (!name) { + return "?"; + } + return name + .split(" ") + .map(n => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + if (!personData) { + return null; + } + + return ( +
+
+
+

Edit Contact

+
+ + +
+
+
+ +
+
+
+ + {getInitials(personData.name as string || "?")} + +
+
+ +
+ + + +
+
Company
+
+ +
+
+ + + +
+
+
+ ); +} + +function EditPersonNameField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "name", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "name", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Name
+
+ +
+
+ ); +} + +function EditPersonJobTitleField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "job_title", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "job_title", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Job Title
+
+ +
+
+ ); +} + +function EditPersonEmailField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "email", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "email", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Email
+
+ +
+
+ ); +} + +function EditPersonLinkedInField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "linkedin_username", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "linkedin_username", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
LinkedIn
+
+ +
+
+ ); +} + +function EditPersonOrganizationSelector({ personId }: { personId: string }) { + const [open, setOpen] = useState(false); + const orgId = persisted.UI.useCell("humans", personId, "org_id", persisted.STORE_ID) as string | null; + const organization = persisted.UI.useRow("organizations", orgId ?? "", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "org_id", + (newOrgId: string | null) => newOrgId ?? "", + [], + persisted.STORE_ID, + ); + + const handleRemoveOrganization = () => { + handleChange(null); + }; + + return ( + + +
+ {organization + ? ( +
+ {organization.name} + + { + e.stopPropagation(); + handleRemoveOrganization(); + }} + /> + +
+ ) + : Select organization} +
+
+ + + setOpen(false)} /> + +
+ ); +} + +function OrganizationControl({ + onChange, + closePopover, +}: { + onChange: (orgId: string | null) => void; + closePopover: () => void; +}) { + const [searchTerm, setSearchTerm] = useState(""); + + const organizationsData = persisted.UI.useResultTable(persisted.QUERIES.visibleOrganizations, persisted.STORE_ID); + + const allOrganizations = Object.entries(organizationsData).map(([id, data]) => ({ + id, + ...(data as any), + })); + + const organizations = searchTerm.trim() + ? allOrganizations.filter((org: any) => org.name.toLowerCase().includes(searchTerm.toLowerCase())) + : allOrganizations; + + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault(); + // Handle submit + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + // Handle enter + } + }; + + const selectOrganization = (orgId: string) => { + onChange(orgId); + closePopover(); + }; + + return ( +
+
Organization
+ +
+
+
+ + + + setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search or add company" + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0" + /> +
+ + {searchTerm.trim() && ( +
+ {organizations.map((org: any) => ( + + ))} + + {organizations.length === 0 && ( + + )} +
+ )} + + {!searchTerm.trim() && organizations.length > 0 && ( +
+ {organizations.map((org: any) => ( + + ))} +
+ )} +
+
+
+ ); +} + +function EditOrganizationForm({ + organization, + onSave, + onCancel, +}: { + organization: any; + onSave: () => void; + onCancel: () => void; +}) { + const name = persisted.UI.useCell("organizations", organization.id, "name", persisted.STORE_ID) as string; + + const handleChange = persisted.UI.useSetCellCallback( + "organizations", + organization.id, + "name", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name?.trim()) { + onSave(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (name?.trim()) { + onSave(); + } + } + if (e.key === "Escape") { + onCancel(); + } + }; + + return ( +
+
+
+ + {name?.trim() && ( + + )} +
+
+
+ ); +} + +function NewOrganizationForm({ + onSave, + onCancel, +}: { + onSave: () => void; + onCancel: () => void; +}) { + const [name, setName] = useState(""); + + const handleAdd = persisted.UI.useAddRowCallback( + "organizations", + () => ({ + name: name.trim(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }), + [name], + persisted.STORE_ID, + () => { + setName(""); + onSave(); + }, + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name.trim()) { + handleAdd(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (name.trim()) { + handleAdd(); + } + } + if (e.key === "Escape") { + onCancel(); + } + }; + + return ( +
+
+
+ setName(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Add organization" + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" + autoFocus + /> + {name.trim() && ( + + )} +
+
+
+ ); } diff --git a/apps/desktop2/src/components/main/body/contacts/temp.tsx b/apps/desktop2/src/components/main/body/contacts/temp.tsx deleted file mode 100644 index cfab58474..000000000 --- a/apps/desktop2/src/components/main/body/contacts/temp.tsx +++ /dev/null @@ -1,1000 +0,0 @@ -// export function ContactView({ userId, initialPersonId, initialOrgId }: ContactViewProps) { -// // Simple state initialization - handles both normal and deep-link cases -// const [selectedOrganization, setSelectedOrganization] = useState(initialOrgId || null); -// const [selectedPerson, setSelectedPerson] = useState(initialPersonId || null); - -// const [editingPerson, setEditingPerson] = useState(null); -// const [editingOrg, setEditingOrg] = useState(null); -// const [showNewOrg, setShowNewOrg] = useState(false); -// const [sortOption, setSortOption] = useState<"alphabetical" | "oldest" | "newest">("alphabetical"); -// const queryClient = useQueryClient(); - -// // Load organizations once and keep cached (global data) -// const { data: organizations = [] } = useQuery({ -// queryKey: ["organizations"], -// queryFn: () => dbCommands.listOrganizations(null), -// }); - -// // Load user's own profile -// const { data: userProfile } = useQuery({ -// queryKey: ["user-profile", userId], -// queryFn: async () => { -// try { -// return await dbCommands.getHuman(userId); -// } catch (error) { -// console.error("Error fetching user profile:", error); -// return null; -// } -// }, -// }); - -// // Load all people once and keep cached (user-specific data) -// const { data: allPeople = [] } = useQuery({ -// queryKey: ["all-people", userId], -// queryFn: async () => { -// try { -// const allHumans = await dbCommands.listHumans(null); -// return allHumans; -// } catch (error) { -// console.error("Error fetching all people:", error); -// return []; -// } -// }, -// }); - -// // Merge user profile with all people, ensuring user's own profile is included -// const allPeopleWithUser = React.useMemo(() => { -// if (!userProfile) { -// return allPeople; -// } - -// // Check if user is already in the list -// const userInList = allPeople.some(person => person.id === userId); - -// if (userInList) { -// return allPeople; -// } else { -// // Add user profile to the beginning of the list -// return [userProfile, ...allPeople]; -// } -// }, [allPeople, userProfile, userId]); - -// // Person sessions - only runs when person is selected -// const { data: personSessions = [] } = useQuery({ -// queryKey: ["person-sessions", selectedPerson || "none"], -// queryFn: async () => { -// if (!selectedPerson) { -// throw new Error("Query should not run when selectedPerson is null"); -// } - -// const sessions = await dbCommands.listSessions({ -// type: "search", -// query: "", -// user_id: userId, -// limit: 100, -// }); - -// const sessionsWithPerson = []; -// for (const session of sessions) { -// try { -// const participants = await dbCommands.sessionListParticipants(session.id); -// if (participants.some(p => p.id === selectedPerson)) { -// sessionsWithPerson.push(session); -// } -// } catch (error) { -// console.error("Error fetching participants for session", session.id, error); -// } -// } - -// return sessionsWithPerson; -// }, -// enabled: selectedPerson !== null && selectedPerson !== undefined && selectedPerson !== "", -// gcTime: 5 * 60 * 1000, -// staleTime: 30 * 1000, -// }); - -// const isValidName = (name: string | null): boolean => { -// return name !== null && name !== "" && name !== "Null"; -// }; - -// const displayPeople = React.useMemo(() => { -// let filtered = (selectedOrganization -// ? allPeopleWithUser.filter(person => person.organization_id === selectedOrganization) -// : allPeopleWithUser).filter(person => person.id === userId || isValidName(person.full_name)); - -// if (sortOption === "alphabetical") { -// filtered = [...filtered].sort((a, b) => { -// const nameA = (a.full_name || a.email || "").toLowerCase(); -// const nameB = (b.full_name || b.email || "").toLowerCase(); -// return nameA.localeCompare(nameB); -// }); -// } else if (sortOption === "newest") { -// filtered = [...filtered].reverse(); -// } - -// return filtered; -// }, [selectedOrganization, allPeopleWithUser, userId, sortOption]); - -// const selectedPersonData = displayPeople.find(p => p.id === selectedPerson); - -// const handleSessionClick = (sessionId: string) => { -// const path = { to: "/app/note/$id", params: { id: sessionId } } as const satisfies LinkProps; - -// windowsCommands.windowShow({ type: "main" }).then(() => { -// windowsCommands.windowEmitNavigate({ type: "main" }, { -// path: path.to.replace("$id", path.params.id), -// search: null, -// }); -// }); -// }; - -// const handleEditPerson = (personId: string) => { -// setEditingPerson(personId); -// }; - -// const handleEditOrganization = (organizationId: string) => { -// setEditingOrg(organizationId); -// }; - -// const deletePersonMutation = useMutation({ -// mutationFn: (personId: string) => dbCommands.deleteHuman(personId), -// onSuccess: () => { -// queryClient.invalidateQueries({ queryKey: ["all-people"] }); -// queryClient.invalidateQueries({ queryKey: ["user-profile"] }); - -// if (selectedPerson === selectedPersonData?.id) { -// setSelectedPerson(null); -// } -// }, -// }); - -// const handleDeletePerson = async (personId: string) => { -// const userConfirmed = await confirm( -// "Are you sure you want to delete this contact? This action cannot be undone.", -// ); -// if (userConfirmed) { -// deletePersonMutation.mutate(personId); -// } -// }; - -// return ( -//
-//
-//
-//

Organizations

-// -//
-//
-//
-// -// {showNewOrg && ( -// { -// setShowNewOrg(false); -// setSelectedOrganization(org.id); -// }} -// onCancel={() => setShowNewOrg(false)} -// /> -// )} -// {organizations.map((org) => ( -// editingOrg === org.id -// ? ( -// setEditingOrg(null)} -// onCancel={() => setEditingOrg(null)} -// /> -// ) -// : ( -//
-// -// -//
-// ) -// ))} -//
-//
-//
- -//
-//
-//

People

-//
-// -// -//
-//
-//
-//
-// {displayPeople.map((person) => ( -// -// ))} -//
-//
-//
- -//
-// {selectedPersonData -// ? ( -// editingPerson === selectedPersonData.id -// ? ( -// setEditingPerson(null)} -// onCancel={() => setEditingPerson(null)} -// /> -// ) -// : ( -// <> -//
-//
-//
-// -// {getInitials(selectedPersonData.full_name || selectedPersonData.email)} -// -//
-//
-//
-//
-//

-// {selectedPersonData.full_name || "Unnamed Contact"} -// {selectedPersonData.id === userId && ( -// You -// )} -//

-// {selectedPersonData.job_title && ( -//

{selectedPersonData.job_title}

-// )} -// {selectedPersonData.email && ( -//

{selectedPersonData.email}

-// )} -// {selectedPersonData.organization_id && ( -// -// )} -// {!selectedPersonData.is_user && selectedPersonData.email && ( -// -// )} -//
-//
-// -// {!selectedPersonData.is_user && ( -// -// )} -//
-//
-//
-//
-//
- -//
-//

Related Notes

-//
-//
-// {personSessions.length > 0 -// ? ( -// personSessions.map((session) => ( -// -// )) -// ) -// :

No related notes found

} -//
-//
-//
-// -// ) -// ) -// : ( -//
-//

Select a person to view details

-//
-// )} -//
-//
-// ); -// } - -// function OrganizationInfo({ organizationId }: { organizationId: string }) { -// const { data: organization } = useQuery({ -// queryKey: ["organization", organizationId], -// queryFn: () => dbCommands.getOrganization(organizationId), -// enabled: !!organizationId, -// }); - -// if (!organization) { -// return null; -// } - -// return ( -//

-// {organization.name} -//

-// ); -// } - -// function EditPersonForm({ -// person, -// organizations, -// onSave, -// onCancel, -// }: { -// person: Human; -// organizations: Organization[]; -// onSave: () => void; -// onCancel: () => void; -// }) { -// const queryClient = useQueryClient(); -// const [formData, setFormData] = useState({ -// full_name: person.full_name || "", -// email: person.email || "", -// job_title: person.job_title || "", -// linkedin_username: person.linkedin_username || "", -// organization_id: person.organization_id, -// }); - -// const updatePersonMutation = useMutation({ -// mutationFn: (data: Partial) => -// dbCommands.upsertHuman({ -// ...person, -// ...data, -// }), -// onSuccess: () => { -// queryClient.invalidateQueries({ queryKey: ["all-people"] }); -// queryClient.invalidateQueries({ queryKey: ["user-profile"] }); -// onSave(); -// }, -// onError: () => { -// console.error("Failed to update contact"); -// }, -// }); - -// const handleSubmit = (e: React.FormEvent) => { -// e.preventDefault(); -// updatePersonMutation.mutate(formData); -// }; - -// return ( -//
-// {/* Header */} -//
-//
-//

Edit Contact

-//
-// -// -//
-//
-//
- -// {/* Content */} -//
-// {/* Avatar Section */} -//
-//
-// -// {getInitials(formData.full_name || "?")} -// -//
-//
- -// {/* Form Section */} -//
-// {/* Name Field */} -//
-//
Name
-//
-// setFormData({ ...formData, full_name: e.target.value })} -// placeholder="John Doe" -// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" -// /> -//
-//
- -// {/* Job Title Field */} -//
-//
Job Title
-//
-// setFormData({ ...formData, job_title: e.target.value })} -// placeholder="Software Engineer" -// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" -// /> -//
-//
- -// {/* Company Field */} -//
-//
Company
-//
-// setFormData({ ...formData, organization_id: orgId })} -// /> -//
-//
- -// {/* Email Field */} -//
-//
Email
-//
-// setFormData({ ...formData, email: e.target.value })} -// placeholder="john@example.com" -// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" -// /> -//
-//
- -// {/* LinkedIn Field */} -//
-//
LinkedIn
-//
-// setFormData({ ...formData, linkedin_username: e.target.value })} -// placeholder="https://www.linkedin.com/in/johntopia/" -// className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" -// /> -//
-//
-//
-//
-//
-// ); -// } - -// function ContactOrganizationSelector({ -// value, -// onChange, -// }: { -// value: string | null; -// onChange: (orgId: string | null) => void; -// }) { -// const [open, setOpen] = useState(false); - -// const { data: organization } = useQuery({ -// queryKey: ["org", value], -// queryFn: () => (value ? dbCommands.getOrganization(value) : null), -// enabled: !!value, -// }); - -// const handleRemoveOrganization = () => { -// onChange(null); -// }; - -// return ( -// -// -//
-// {organization -// ? ( -//
-// {organization.name} -// -// { -// e.stopPropagation(); -// handleRemoveOrganization(); -// }} -// /> -// -//
-// ) -// : Select organization} -//
-//
- -// -// setOpen(false)} /> -// -//
-// ); -// } - -// function OrganizationControl({ -// onChange, -// closePopover, -// }: { -// onChange: (orgId: string | null) => void; -// closePopover: () => void; -// }) { -// const queryClient = useQueryClient(); -// const [searchTerm, setSearchTerm] = useState(""); - -// const addOrganizationMutation = useMutation({ -// mutationFn: async ({ name }: { name: string }) => { -// const newOrg = await dbCommands.upsertOrganization({ -// id: crypto.randomUUID(), -// name, -// description: null, -// }); - -// onChange(newOrg.id); -// return newOrg; -// }, -// onSuccess: () => { -// queryClient.invalidateQueries({ -// queryKey: ["organizations"], -// }); -// closePopover(); -// }, -// }); - -// const { data: organizations = [] } = useQuery({ -// queryKey: ["organizations", searchTerm], -// queryFn: () => { -// if (!searchTerm) { -// return dbCommands.listOrganizations(null); -// } -// return dbCommands.listOrganizations({ search: [5, searchTerm] }); -// }, -// }); - -// const handleSubmit = (e: React.SyntheticEvent) => { -// e.preventDefault(); - -// const name = searchTerm.trim(); -// if (name === "") { -// return; -// } - -// addOrganizationMutation.mutate({ name }); -// setSearchTerm(""); -// }; - -// const handleKeyDown = (e: React.KeyboardEvent) => { -// if (e.key === "Enter") { -// e.preventDefault(); - -// const name = searchTerm.trim(); -// if (name === "") { -// return; -// } - -// addOrganizationMutation.mutate({ name }); -// setSearchTerm(""); -// } -// }; - -// const selectOrganization = (orgId: string) => { -// onChange(orgId); -// closePopover(); -// }; - -// return ( -//
-//
Organization
- -//
-//
-//
-// -// -// -// setSearchTerm(e.target.value)} -// onKeyDown={handleKeyDown} -// placeholder="Search or add company" -// className="w-full bg-transparent text-sm focus:outline-none placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0" -// /> -//
- -// {searchTerm.trim() && ( -//
-// {organizations.map((org) => ( -// -// ))} - -// {organizations.length === 0 && ( -// -// )} -//
-// )} - -// {!searchTerm.trim() && organizations.length > 0 && ( -//
-// {organizations.map((org) => ( -// -// ))} -//
-// )} -//
-//
-//
-// ); -// } - -// function EditOrganizationForm({ -// organization, -// onSave, -// onCancel, -// }: { -// organization: Organization; -// onSave: () => void; -// onCancel: () => void; -// }) { -// const queryClient = useQueryClient(); -// const [name, setName] = useState(organization.name); -// const formRef = useRef(null); - -// const updateOrgMutation = useMutation({ -// mutationFn: (data: Partial) => -// dbCommands.upsertOrganization({ -// ...organization, -// ...data, -// }), -// onSuccess: () => { -// queryClient.invalidateQueries({ queryKey: ["organizations"] }); -// queryClient.invalidateQueries({ queryKey: ["organization", organization.id] }); -// onSave(); -// }, -// onError: () => { -// console.error("Failed to update organization"); -// }, -// }); - -// useEffect(() => { -// const handleClickOutside = (event: MouseEvent) => { -// if (formRef.current && !formRef.current.contains(event.target as Node)) { -// onCancel(); -// } -// }; - -// document.addEventListener("mousedown", handleClickOutside); -// return () => { -// document.removeEventListener("mousedown", handleClickOutside); -// }; -// }, [onCancel]); - -// const handleSubmit = (e: React.FormEvent) => { -// e.preventDefault(); -// if (name.trim()) { -// updateOrgMutation.mutate({ name: name.trim() }); -// } -// }; - -// const handleKeyDown = (e: React.KeyboardEvent) => { -// if (e.key === "Enter") { -// e.preventDefault(); -// if (name.trim()) { -// updateOrgMutation.mutate({ name: name.trim() }); -// } -// } -// if (e.key === "Escape") { -// onCancel(); -// } -// }; - -// return ( -//
-//
-//
-// setName(e.target.value)} -// onKeyDown={handleKeyDown} -// placeholder="Organization name" -// className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" -// autoFocus -// /> -// {name.trim() && ( -// -// )} -//
-//
-//
-// ); -// } - -// function NewOrganizationForm({ -// onSave, -// onCancel, -// }: { -// onSave: (org: Organization) => void; -// onCancel: () => void; -// }) { -// const queryClient = useQueryClient(); -// const [name, setName] = useState(""); -// const formRef = useRef(null); - -// const createOrgMutation = useMutation({ -// mutationFn: (name: string) => -// dbCommands.upsertOrganization({ -// id: crypto.randomUUID(), -// name, -// description: null, -// }), -// onSuccess: (org) => { -// queryClient.invalidateQueries({ queryKey: ["organizations"] }); -// onSave(org); -// }, -// onError: () => { -// console.error("Failed to create organization"); -// }, -// }); - -// useEffect(() => { -// const handleClickOutside = (event: MouseEvent) => { -// if (formRef.current && !formRef.current.contains(event.target as Node)) { -// onCancel(); -// } -// }; - -// document.addEventListener("mousedown", handleClickOutside); -// return () => { -// document.removeEventListener("mousedown", handleClickOutside); -// }; -// }, [onCancel]); - -// const handleSubmit = (e: React.FormEvent) => { -// e.preventDefault(); -// if (name.trim()) { -// createOrgMutation.mutate(name.trim()); -// } -// }; - -// const handleKeyDown = (e: React.KeyboardEvent) => { -// if (e.key === "Enter") { -// e.preventDefault(); -// if (name.trim()) { -// createOrgMutation.mutate(name.trim()); -// } -// } -// if (e.key === "Escape") { -// onCancel(); -// } -// }; - -// return ( -//
-//
-//
-// setName(e.target.value)} -// onKeyDown={handleKeyDown} -// placeholder="Add organization" -// className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" -// autoFocus -// /> -// {name.trim() && ( -// -// )} -//
-//
-//
-// ); -// } diff --git a/apps/desktop2/src/components/main/body/shared.tsx b/apps/desktop2/src/components/main/body/shared.tsx index 87d49ce21..ae4ef1a9a 100644 --- a/apps/desktop2/src/components/main/body/shared.tsx +++ b/apps/desktop2/src/components/main/body/shared.tsx @@ -18,10 +18,10 @@ export function TabItemBase( }, ) { return ( - - + ); } diff --git a/apps/desktop2/src/components/main/sidebar/profile/banner.tsx b/apps/desktop2/src/components/main/sidebar/profile/banner.tsx index 2ed66bf01..7ee696358 100644 --- a/apps/desktop2/src/components/main/sidebar/profile/banner.tsx +++ b/apps/desktop2/src/components/main/sidebar/profile/banner.tsx @@ -8,36 +8,30 @@ export function Trial() { }, []); return ( -
-
- - Try Hyprnote Pro +
+ + Hyprnote Pro
- -
- Experience smarter meetings with free trial. +
+ Free trial for smarter meetings
-
+
+ Start Trial + +
+ ); } diff --git a/apps/desktop2/src/store/seed.ts b/apps/desktop2/src/store/seed.ts index 3f76cf4e3..3756584c3 100644 --- a/apps/desktop2/src/store/seed.ts +++ b/apps/desktop2/src/store/seed.ts @@ -38,17 +38,39 @@ const createOrganization = () => ({ } satisfies Organization, }); -const createHuman = (org_id: string) => { +const createHuman = (org_id: string, isUser = false) => { const sex = faker.person.sexType(); const firstName = faker.person.firstName(sex); const lastName = faker.person.lastName(); + const jobTitles = [ + "Software Engineer", + "Product Manager", + "Designer", + "Engineering Manager", + "CEO", + "CTO", + "VP of Engineering", + "Data Scientist", + "Marketing Manager", + "Sales Director", + "Account Executive", + "Customer Success Manager", + "Operations Manager", + "HR Manager", + ]; + return { id: id(), data: { user_id: USER_ID, name: `${firstName} ${lastName}`, email: faker.internet.email({ firstName, lastName }), + job_title: faker.helpers.arrayElement(jobTitles), + linkedin_username: faker.datatype.boolean({ probability: 0.7 }) + ? `${firstName.toLowerCase()}${lastName.toLowerCase()}` + : undefined, + is_user: isUser, created_at: faker.date.past({ years: 1 }).toISOString(), org_id, } satisfies Human, @@ -342,6 +364,15 @@ const generateMockData = (config: MockConfig) => { }); const humanIds: string[] = []; + + // Create the current user first (in the first organization) + if (orgIds.length > 0) { + const currentUser = createHuman(orgIds[0], true); + humans[currentUser.id] = currentUser.data; + humanIds.push(currentUser.id); + } + + // Create other humans orgIds.forEach((orgId) => { const humanCount = faker.number.int({ min: config.humansPerOrg.min, @@ -349,7 +380,7 @@ const generateMockData = (config: MockConfig) => { }); Array.from({ length: humanCount }, () => { - const human = createHuman(orgId); + const human = createHuman(orgId, false); humans[human.id] = human.data; humanIds.push(human.id); }); @@ -477,8 +508,8 @@ faker.seed(123); export const V1 = (() => { const data = generateMockData({ - organizations: 5, - humansPerOrg: { min: 3, max: 8 }, + organizations: 8, + humansPerOrg: { min: 5, max: 12 }, sessionsPerHuman: { min: 2, max: 6 }, eventsPerHuman: { min: 1, max: 5 }, calendarsPerUser: 3, @@ -501,7 +532,14 @@ export const V1 = (() => { (s) => !s.event_id, ).length; + const totalHumans = Object.keys(data.humans).length; + const totalOrganizations = Object.keys(data.organizations).length; + const totalUsers = Object.values(data.humans).filter((h) => h.is_user).length; + console.log("=== Seed Data Statistics ==="); + console.log(`Total Organizations: ${totalOrganizations}`); + console.log(`Total Humans: ${totalHumans}`); + console.log(`Current User(s): ${totalUsers}`); console.log(`Total Events: ${totalEvents}`); console.log(`Total Sessions: ${totalSessions}`); console.log(`Events without Session: ${eventsWithoutSession}`); diff --git a/apps/desktop2/src/store/tinybase/persisted.ts b/apps/desktop2/src/store/tinybase/persisted.ts index 7dc04e008..b526c7b2d 100644 --- a/apps/desktop2/src/store/tinybase/persisted.ts +++ b/apps/desktop2/src/store/tinybase/persisted.ts @@ -38,7 +38,12 @@ import { type InferTinyBaseSchema, jsonObject, type ToStorageType } from "./shar export const STORE_ID = "persisted"; -export const humanSchema = baseHumanSchema.omit({ id: true }).extend({ created_at: z.string() }); +export const humanSchema = baseHumanSchema.omit({ id: true }).extend({ + created_at: z.string(), + job_title: z.preprocess(val => val ?? undefined, z.string().optional()), + linkedin_username: z.preprocess(val => val ?? undefined, z.string().optional()), + is_user: z.preprocess(val => val ?? undefined, z.boolean().optional()), +}); export const eventSchema = baseEventSchema.omit({ id: true }).extend({ created_at: z.string(), @@ -148,6 +153,9 @@ const SCHEMA = { name: { type: "string" }, email: { type: "string" }, org_id: { type: "string" }, + job_title: { type: "string" }, + linkedin_username: { type: "string" }, + is_user: { type: "boolean" }, } satisfies InferTinyBaseSchema, organizations: { user_id: { type: "string" }, @@ -396,6 +404,27 @@ export const StoreComponent = () => { join("events", "event_id").as("event"); select("event", "started_at").as("event_started_at"); }, + ) + .setQueryDefinition( + QUERIES.visibleHumans, + "humans", + ({ select }) => { + select("name"); + select("email"); + select("org_id"); + select("job_title"); + select("linkedin_username"); + select("is_user"); + select("created_at"); + }, + ) + .setQueryDefinition( + QUERIES.visibleOrganizations, + "organizations", + ({ select }) => { + select("name"); + select("created_at"); + }, ), [store2], )!; diff --git a/apps/desktop2/src/store/zustand/tabs.ts b/apps/desktop2/src/store/zustand/tabs.ts index f9b27d808..ef81adcad 100644 --- a/apps/desktop2/src/store/zustand/tabs.ts +++ b/apps/desktop2/src/store/zustand/tabs.ts @@ -9,10 +9,10 @@ type State = { }; type Actions = - & TabUpdator + & TabUpdater & TabStateUpdater; -type TabUpdator = { +type TabUpdater = { setTabs: (tabs: Tab[]) => void; openCurrent: (tab: Tab) => void; openNew: (tab: Tab) => void; @@ -22,6 +22,7 @@ type TabUpdator = { }; type TabStateUpdater = { + updateContactsTabState: (tab: Tab, state: Extract["state"]) => void; updateSessionTabState: (tab: Tab, state: Extract["state"]) => void; }; @@ -30,37 +31,42 @@ type Store = State & Actions; export const useTabs = create((set, get, _store) => ({ currentTab: null, tabs: [], - setTabs: (tabs) => set({ tabs, currentTab: tabs.find((t) => t.active) || null }), + setTabs: (tabs) => { + const tabsWithDefaults = tabs.map(t => tabSchema.parse(t)); + set({ tabs: tabsWithDefaults, currentTab: tabsWithDefaults.find((t) => t.active) || null }); + }, openCurrent: (newTab) => { const { tabs } = get(); + const tabWithDefaults = tabSchema.parse(newTab); const existingTabIdx = tabs.findIndex((t) => t.active); if (existingTabIdx === -1) { const nextTabs = tabs - .filter((t) => !isSameTab(t, newTab)) + .filter((t) => !isSameTab(t, tabWithDefaults)) .map((t) => ({ ...t, active: false })) - .concat([{ ...newTab, active: true }]); - set({ tabs: nextTabs, currentTab: newTab }); + .concat([{ ...tabWithDefaults, active: true }]); + set({ tabs: nextTabs, currentTab: tabWithDefaults }); } else { const nextTabs = tabs .map((t, idx) => idx === existingTabIdx - ? { ...newTab, active: true } - : isSameTab(t, newTab) + ? { ...tabWithDefaults, active: true } + : isSameTab(t, tabWithDefaults) ? null : { ...t, active: false } ) .filter((t): t is Tab => t !== null); - set({ tabs: nextTabs, currentTab: newTab }); + set({ tabs: nextTabs, currentTab: tabWithDefaults }); } }, openNew: (tab) => { const { tabs } = get(); + const tabWithDefaults = tabSchema.parse(tab); const nextTabs = tabs - .filter((t) => !isSameTab(t, tab)) + .filter((t) => !isSameTab(t, tabWithDefaults)) .map((t) => ({ ...t, active: false })) - .concat([{ ...tab, active: true }]); - set({ tabs: nextTabs, currentTab: tab }); + .concat([{ ...tabWithDefaults, active: true }]); + set({ tabs: nextTabs, currentTab: tabWithDefaults }); }, select: (tab) => { const { tabs } = get(); @@ -99,6 +105,19 @@ export const useTabs = create((set, get, _store) => ({ : currentTab; set({ tabs: nextTabs, currentTab: nextCurrentTab }); }, + updateContactsTabState: (tab, state) => { + const { tabs, currentTab } = get(); + const nextTabs = tabs.map((t) => + isSameTab(t, tab) && t.type === "contacts" + ? { ...t, state } + : t + ); + + const nextCurrentTab = currentTab && isSameTab(currentTab, tab) && currentTab.type === "contacts" + ? { ...currentTab, state } + : currentTab; + set({ tabs: nextTabs, currentTab: nextCurrentTab }); + }, })); const baseTabSchema = z.object({ @@ -113,6 +132,24 @@ export const tabSchema = z.discriminatedUnion("type", [ editor: z.enum(["raw", "enhanced", "transcript"]).default("raw"), }).default({ editor: "raw" }), }), + baseTabSchema.extend({ + type: z.literal("contacts"), + state: z.object({ + selectedOrganization: z.string().nullable().default(null), + selectedPerson: z.string().nullable().default(null), + editingPerson: z.string().nullable().default(null), + editingOrg: z.string().nullable().default(null), + showNewOrg: z.boolean().default(false), + sortOption: z.enum(["alphabetical", "oldest", "newest"]).default("alphabetical"), + }).default({ + selectedOrganization: null, + selectedPerson: null, + editingPerson: null, + editingOrg: null, + showNewOrg: false, + sortOption: "alphabetical", + }), + }), baseTabSchema.extend({ type: z.literal("events" satisfies typeof TABLES[number]), id: z.string(), @@ -129,9 +166,7 @@ export const tabSchema = z.discriminatedUnion("type", [ type: z.literal("folders" satisfies typeof TABLES[number]), id: z.string().nullable(), }), - baseTabSchema.extend({ - type: z.literal("contacts"), - }), + baseTabSchema.extend({ type: z.literal("calendars"), month: z.coerce.date(), diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index d0cb41017..e762a44a0 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -15,6 +15,9 @@ export const humans = pgTable(TABLE_HUMANS, { name: text("name").notNull(), email: text("email").notNull(), org_id: uuid("org_id").notNull(), + job_title: text("job_title"), + linkedin_username: text("linkedin_username"), + is_user: boolean("is_user"), }); export const TABLE_ORGANIZATIONS = "organizations"; From c484c469496a7a5f1ae09dfa3238264224af0876 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 10 Oct 2025 12:52:57 +0900 Subject: [PATCH 5/6] kind of done --- .../components/main/body/contacts/details.tsx | 515 +++++++++++ .../components/main/body/contacts/index.tsx | 845 +----------------- .../main/body/contacts/organizations.tsx | 234 +++++ .../components/main/body/contacts/people.tsx | 85 ++ 4 files changed, 838 insertions(+), 841 deletions(-) create mode 100644 apps/desktop2/src/components/main/body/contacts/details.tsx create mode 100644 apps/desktop2/src/components/main/body/contacts/organizations.tsx create mode 100644 apps/desktop2/src/components/main/body/contacts/people.tsx diff --git a/apps/desktop2/src/components/main/body/contacts/details.tsx b/apps/desktop2/src/components/main/body/contacts/details.tsx new file mode 100644 index 000000000..efe906086 --- /dev/null +++ b/apps/desktop2/src/components/main/body/contacts/details.tsx @@ -0,0 +1,515 @@ +import { Building2, CircleMinus, FileText, Pencil, SearchIcon, TrashIcon, UserPlus } from "lucide-react"; +import React, { useState } from "react"; + +import { Button } from "@hypr/ui/components/ui/button"; +import { Input } from "@hypr/ui/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; +import * as persisted from "../../../../store/tinybase/persisted"; + +export function DetailsColumn({ + selectedPersonData, + editingPerson, + setEditingPerson, + organizations, + personSessions, + handleEditPerson, + handleDeletePerson, + handleSessionClick, + getInitials, +}: { + selectedPersonData: any; + editingPerson: string | null; + setEditingPerson: (id: string | null) => void; + organizations: any[]; + personSessions: any[]; + handleEditPerson: (id: string) => void; + handleDeletePerson: (id: string) => void; + handleSessionClick: (id: string) => void; + getInitials: (name: string | null) => string; +}) { + return ( +
+ {selectedPersonData + ? ( + editingPerson === selectedPersonData.id + ? ( + setEditingPerson(null)} + onCancel={() => setEditingPerson(null)} + /> + ) + : ( + <> +
+
+
+ + {getInitials(selectedPersonData.name || selectedPersonData.email)} + +
+
+
+
+

+ {selectedPersonData.name || "Unnamed Contact"} + {selectedPersonData.is_user && ( + You + )} +

+ {selectedPersonData.job_title && ( +

{selectedPersonData.job_title}

+ )} + {selectedPersonData.email && ( +

{selectedPersonData.email}

+ )} + {selectedPersonData.org_id && } + {!selectedPersonData.is_user && selectedPersonData.email && ( + + )} +
+
+ + {!selectedPersonData.is_user && ( + + )} +
+
+
+
+
+ +
+

Related Notes

+
+
+ {personSessions.length > 0 + ? ( + personSessions.map((session: any) => ( + + )) + ) + :

No related notes found

} +
+
+
+ + ) + ) + : ( +
+

Select a person to view details

+
+ )} +
+ ); +} + +function OrganizationInfo({ organizationId }: { organizationId: string }) { + const organization = persisted.UI.useRow("organizations", organizationId, persisted.STORE_ID); + + if (!organization) { + return null; + } + + return ( +

+ {organization.name} +

+ ); +} + +function EditPersonForm({ + person, + organizations: _organizations, + onSave, + onCancel, +}: { + person: any; + organizations: any[]; + onSave: () => void; + onCancel: () => void; +}) { + const personData = persisted.UI.useRow("humans", person.id, persisted.STORE_ID); + + const getInitials = (name: string | null) => { + if (!name) { + return "?"; + } + return name + .split(" ") + .map(n => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + if (!personData) { + return null; + } + + return ( +
+
+
+

Edit Contact

+
+ + +
+
+
+ +
+
+
+ + {getInitials(personData.name as string || "?")} + +
+
+ +
+ + + +
+
Company
+
+ +
+
+ + + +
+
+
+ ); +} + +function EditPersonNameField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "name", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "name", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Name
+
+ +
+
+ ); +} + +function EditPersonJobTitleField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "job_title", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "job_title", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Job Title
+
+ +
+
+ ); +} + +function EditPersonEmailField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "email", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "email", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
Email
+
+ +
+
+ ); +} + +function EditPersonLinkedInField({ personId }: { personId: string }) { + const value = persisted.UI.useCell("humans", personId, "linkedin_username", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "linkedin_username", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + return ( +
+
LinkedIn
+
+ +
+
+ ); +} + +function EditPersonOrganizationSelector({ personId }: { personId: string }) { + const [open, setOpen] = useState(false); + const orgId = persisted.UI.useCell("humans", personId, "org_id", persisted.STORE_ID) as string | null; + const organization = persisted.UI.useRow("organizations", orgId ?? "", persisted.STORE_ID); + + const handleChange = persisted.UI.useSetCellCallback( + "humans", + personId, + "org_id", + (newOrgId: string | null) => newOrgId ?? "", + [], + persisted.STORE_ID, + ); + + const handleRemoveOrganization = () => { + handleChange(null); + }; + + return ( + + +
+ {organization + ? ( +
+ {organization.name} + + { + e.stopPropagation(); + handleRemoveOrganization(); + }} + /> + +
+ ) + : Select organization} +
+
+ + + setOpen(false)} /> + +
+ ); +} + +function OrganizationControl({ + onChange, + closePopover, +}: { + onChange: (orgId: string | null) => void; + closePopover: () => void; +}) { + const [searchTerm, setSearchTerm] = useState(""); + + const organizationsData = persisted.UI.useResultTable(persisted.QUERIES.visibleOrganizations, persisted.STORE_ID); + + const allOrganizations = Object.entries(organizationsData).map(([id, data]) => ({ + id, + ...(data as any), + })); + + const organizations = searchTerm.trim() + ? allOrganizations.filter((org: any) => org.name.toLowerCase().includes(searchTerm.toLowerCase())) + : allOrganizations; + + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + } + }; + + const selectOrganization = (orgId: string) => { + onChange(orgId); + closePopover(); + }; + + return ( +
+
Organization
+ +
+
+
+ + + + setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search or add company" + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0" + /> +
+ + {searchTerm.trim() && ( +
+ {organizations.map((org: any) => ( + + ))} + + {organizations.length === 0 && ( + + )} +
+ )} + + {!searchTerm.trim() && organizations.length > 0 && ( +
+ {organizations.map((org: any) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/apps/desktop2/src/components/main/body/contacts/index.tsx b/apps/desktop2/src/components/main/body/contacts/index.tsx index 6f26b3873..cba691924 100644 --- a/apps/desktop2/src/components/main/body/contacts/index.tsx +++ b/apps/desktop2/src/components/main/body/contacts/index.tsx @@ -1,26 +1,11 @@ -import { - Building2, - CircleMinus, - Contact2Icon, - CornerDownLeft, - FileText, - Pencil, - Plus, - SearchIcon, - TrashIcon, - User, - UserPlus, -} from "lucide-react"; -import React, { useState } from "react"; +import { Contact2Icon } from "lucide-react"; -import { Button } from "@hypr/ui/components/ui/button"; -import { Input } from "@hypr/ui/components/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; -import { cn } from "@hypr/ui/lib/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { type Tab, useTabs } from "../../../../store/zustand/tabs"; import { type TabItem, TabItemBase } from "../shared"; +import { DetailsColumn } from "./details"; +import { OrganizationsColumn } from "./organizations"; +import { PeopleColumn } from "./people"; export const TabItemContact: TabItem = ({ tab, handleClose, handleSelect }) => { return ( @@ -184,825 +169,3 @@ function ContactView({ tab }: { tab: Tab }) {
); } - -function OrganizationsColumn({ - selectedOrganization, - setSelectedOrganization, - showNewOrg, - setShowNewOrg, - editingOrg, - setEditingOrg, - organizations, - handleEditOrganization, -}: { - selectedOrganization: string | null; - setSelectedOrganization: (id: string | null) => void; - showNewOrg: boolean; - setShowNewOrg: (show: boolean) => void; - editingOrg: string | null; - setEditingOrg: (id: string | null) => void; - organizations: any[]; - handleEditOrganization: (id: string) => void; -}) { - return ( -
-
-

Organizations

- -
-
-
- - {showNewOrg && ( - setShowNewOrg(false)} - onCancel={() => setShowNewOrg(false)} - /> - )} - {organizations.map((org: any) => - editingOrg === org.id - ? ( - setEditingOrg(null)} - onCancel={() => setEditingOrg(null)} - /> - ) - : ( -
- - -
- ) - )} -
-
-
- ); -} - -function PeopleColumn({ - displayPeople, - selectedPerson, - setSelectedPerson, - sortOption, - setSortOption, - getInitials, -}: { - displayPeople: any[]; - selectedPerson: string | null; - setSelectedPerson: (id: string | null) => void; - sortOption: "alphabetical" | "oldest" | "newest"; - setSortOption: (option: "alphabetical" | "oldest" | "newest") => void; - getInitials: (name: string | null) => string; -}) { - return ( -
-
-

People

-
- - -
-
-
-
- {displayPeople.map((person: any) => ( - - ))} -
-
-
- ); -} - -function DetailsColumn({ - selectedPersonData, - editingPerson, - setEditingPerson, - organizations, - personSessions, - handleEditPerson, - handleDeletePerson, - handleSessionClick, - getInitials, -}: { - selectedPersonData: any; - editingPerson: string | null; - setEditingPerson: (id: string | null) => void; - organizations: any[]; - personSessions: any[]; - handleEditPerson: (id: string) => void; - handleDeletePerson: (id: string) => void; - handleSessionClick: (id: string) => void; - getInitials: (name: string | null) => string; -}) { - return ( -
- {selectedPersonData - ? ( - editingPerson === selectedPersonData.id - ? ( - setEditingPerson(null)} - onCancel={() => setEditingPerson(null)} - /> - ) - : ( - <> -
-
-
- - {getInitials(selectedPersonData.name || selectedPersonData.email)} - -
-
-
-
-

- {selectedPersonData.name || "Unnamed Contact"} - {selectedPersonData.is_user && ( - You - )} -

- {selectedPersonData.job_title && ( -

{selectedPersonData.job_title}

- )} - {selectedPersonData.email && ( -

{selectedPersonData.email}

- )} - {selectedPersonData.org_id && } - {!selectedPersonData.is_user && selectedPersonData.email && ( - - )} -
-
- - {!selectedPersonData.is_user && ( - - )} -
-
-
-
-
- -
-

Related Notes

-
-
- {personSessions.length > 0 - ? ( - personSessions.map((session: any) => ( - - )) - ) - :

No related notes found

} -
-
-
- - ) - ) - : ( -
-

Select a person to view details

-
- )} -
- ); -} - -function OrganizationInfo({ organizationId }: { organizationId: string }) { - const organization = persisted.UI.useRow("organizations", organizationId, persisted.STORE_ID); - - if (!organization) { - return null; - } - - return ( -

- {organization.name} -

- ); -} - -function EditPersonForm({ - person, - organizations: _organizations, - onSave, - onCancel, -}: { - person: any; - organizations: any[]; - onSave: () => void; - onCancel: () => void; -}) { - const personData = persisted.UI.useRow("humans", person.id, persisted.STORE_ID); - - const getInitials = (name: string | null) => { - if (!name) { - return "?"; - } - return name - .split(" ") - .map(n => n[0]) - .join("") - .toUpperCase() - .slice(0, 2); - }; - - if (!personData) { - return null; - } - - return ( -
-
-
-

Edit Contact

-
- - -
-
-
- -
-
-
- - {getInitials(personData.name as string || "?")} - -
-
- -
- - - -
-
Company
-
- -
-
- - - -
-
-
- ); -} - -function EditPersonNameField({ personId }: { personId: string }) { - const value = persisted.UI.useCell("humans", personId, "name", persisted.STORE_ID); - - const handleChange = persisted.UI.useSetCellCallback( - "humans", - personId, - "name", - (e: React.ChangeEvent) => e.target.value, - [], - persisted.STORE_ID, - ); - - return ( -
-
Name
-
- -
-
- ); -} - -function EditPersonJobTitleField({ personId }: { personId: string }) { - const value = persisted.UI.useCell("humans", personId, "job_title", persisted.STORE_ID); - - const handleChange = persisted.UI.useSetCellCallback( - "humans", - personId, - "job_title", - (e: React.ChangeEvent) => e.target.value, - [], - persisted.STORE_ID, - ); - - return ( -
-
Job Title
-
- -
-
- ); -} - -function EditPersonEmailField({ personId }: { personId: string }) { - const value = persisted.UI.useCell("humans", personId, "email", persisted.STORE_ID); - - const handleChange = persisted.UI.useSetCellCallback( - "humans", - personId, - "email", - (e: React.ChangeEvent) => e.target.value, - [], - persisted.STORE_ID, - ); - - return ( -
-
Email
-
- -
-
- ); -} - -function EditPersonLinkedInField({ personId }: { personId: string }) { - const value = persisted.UI.useCell("humans", personId, "linkedin_username", persisted.STORE_ID); - - const handleChange = persisted.UI.useSetCellCallback( - "humans", - personId, - "linkedin_username", - (e: React.ChangeEvent) => e.target.value, - [], - persisted.STORE_ID, - ); - - return ( -
-
LinkedIn
-
- -
-
- ); -} - -function EditPersonOrganizationSelector({ personId }: { personId: string }) { - const [open, setOpen] = useState(false); - const orgId = persisted.UI.useCell("humans", personId, "org_id", persisted.STORE_ID) as string | null; - const organization = persisted.UI.useRow("organizations", orgId ?? "", persisted.STORE_ID); - - const handleChange = persisted.UI.useSetCellCallback( - "humans", - personId, - "org_id", - (newOrgId: string | null) => newOrgId ?? "", - [], - persisted.STORE_ID, - ); - - const handleRemoveOrganization = () => { - handleChange(null); - }; - - return ( - - -
- {organization - ? ( -
- {organization.name} - - { - e.stopPropagation(); - handleRemoveOrganization(); - }} - /> - -
- ) - : Select organization} -
-
- - - setOpen(false)} /> - -
- ); -} - -function OrganizationControl({ - onChange, - closePopover, -}: { - onChange: (orgId: string | null) => void; - closePopover: () => void; -}) { - const [searchTerm, setSearchTerm] = useState(""); - - const organizationsData = persisted.UI.useResultTable(persisted.QUERIES.visibleOrganizations, persisted.STORE_ID); - - const allOrganizations = Object.entries(organizationsData).map(([id, data]) => ({ - id, - ...(data as any), - })); - - const organizations = searchTerm.trim() - ? allOrganizations.filter((org: any) => org.name.toLowerCase().includes(searchTerm.toLowerCase())) - : allOrganizations; - - const handleSubmit = (e: React.SyntheticEvent) => { - e.preventDefault(); - // Handle submit - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - // Handle enter - } - }; - - const selectOrganization = (orgId: string) => { - onChange(orgId); - closePopover(); - }; - - return ( -
-
Organization
- -
-
-
- - - - setSearchTerm(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Search or add company" - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0" - /> -
- - {searchTerm.trim() && ( -
- {organizations.map((org: any) => ( - - ))} - - {organizations.length === 0 && ( - - )} -
- )} - - {!searchTerm.trim() && organizations.length > 0 && ( -
- {organizations.map((org: any) => ( - - ))} -
- )} -
-
-
- ); -} - -function EditOrganizationForm({ - organization, - onSave, - onCancel, -}: { - organization: any; - onSave: () => void; - onCancel: () => void; -}) { - const name = persisted.UI.useCell("organizations", organization.id, "name", persisted.STORE_ID) as string; - - const handleChange = persisted.UI.useSetCellCallback( - "organizations", - organization.id, - "name", - (e: React.ChangeEvent) => e.target.value, - [], - persisted.STORE_ID, - ); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (name?.trim()) { - onSave(); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - if (name?.trim()) { - onSave(); - } - } - if (e.key === "Escape") { - onCancel(); - } - }; - - return ( -
-
-
- - {name?.trim() && ( - - )} -
-
-
- ); -} - -function NewOrganizationForm({ - onSave, - onCancel, -}: { - onSave: () => void; - onCancel: () => void; -}) { - const [name, setName] = useState(""); - - const handleAdd = persisted.UI.useAddRowCallback( - "organizations", - () => ({ - name: name.trim(), - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }), - [name], - persisted.STORE_ID, - () => { - setName(""); - onSave(); - }, - ); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (name.trim()) { - handleAdd(); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - if (name.trim()) { - handleAdd(); - } - } - if (e.key === "Escape") { - onCancel(); - } - }; - - return ( -
-
-
- setName(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Add organization" - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" - autoFocus - /> - {name.trim() && ( - - )} -
-
-
- ); -} diff --git a/apps/desktop2/src/components/main/body/contacts/organizations.tsx b/apps/desktop2/src/components/main/body/contacts/organizations.tsx new file mode 100644 index 000000000..0fc3c6f3e --- /dev/null +++ b/apps/desktop2/src/components/main/body/contacts/organizations.tsx @@ -0,0 +1,234 @@ +import { Building2, CornerDownLeft, Pencil, Plus, User } from "lucide-react"; +import React, { useState } from "react"; + +import { cn } from "@hypr/ui/lib/utils"; +import * as persisted from "../../../../store/tinybase/persisted"; + +export function OrganizationsColumn({ + selectedOrganization, + setSelectedOrganization, + showNewOrg, + setShowNewOrg, + editingOrg, + setEditingOrg, + organizations, + handleEditOrganization, +}: { + selectedOrganization: string | null; + setSelectedOrganization: (id: string | null) => void; + showNewOrg: boolean; + setShowNewOrg: (show: boolean) => void; + editingOrg: string | null; + setEditingOrg: (id: string | null) => void; + organizations: any[]; + handleEditOrganization: (id: string) => void; +}) { + return ( +
+
+

Organizations

+ +
+
+
+ + {showNewOrg && ( + setShowNewOrg(false)} + onCancel={() => setShowNewOrg(false)} + /> + )} + {organizations.map((org: any) => + editingOrg === org.id + ? ( + setEditingOrg(null)} + onCancel={() => setEditingOrg(null)} + /> + ) + : ( +
+ + +
+ ) + )} +
+
+
+ ); +} + +function EditOrganizationForm({ + organization, + onSave, + onCancel, +}: { + organization: any; + onSave: () => void; + onCancel: () => void; +}) { + const name = persisted.UI.useCell("organizations", organization.id, "name", persisted.STORE_ID) as string; + + const handleChange = persisted.UI.useSetCellCallback( + "organizations", + organization.id, + "name", + (e: React.ChangeEvent) => e.target.value, + [], + persisted.STORE_ID, + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name?.trim()) { + onSave(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (name?.trim()) { + onSave(); + } + } + if (e.key === "Escape") { + onCancel(); + } + }; + + return ( +
+
+
+ + {name?.trim() && ( + + )} +
+
+
+ ); +} + +function NewOrganizationForm({ + onSave, + onCancel, +}: { + onSave: () => void; + onCancel: () => void; +}) { + const [name, setName] = useState(""); + + const handleAdd = persisted.UI.useAddRowCallback( + "organizations", + () => ({ + name: name.trim(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }), + [name], + persisted.STORE_ID, + () => { + setName(""); + onSave(); + }, + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name.trim()) { + handleAdd(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (name.trim()) { + handleAdd(); + } + } + if (e.key === "Escape") { + onCancel(); + } + }; + + return ( +
+
+
+ setName(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Add organization" + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-400" + autoFocus + /> + {name.trim() && ( + + )} +
+
+
+ ); +} diff --git a/apps/desktop2/src/components/main/body/contacts/people.tsx b/apps/desktop2/src/components/main/body/contacts/people.tsx new file mode 100644 index 000000000..9b2f46d85 --- /dev/null +++ b/apps/desktop2/src/components/main/body/contacts/people.tsx @@ -0,0 +1,85 @@ +import { Plus } from "lucide-react"; + +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; +import { cn } from "@hypr/ui/lib/utils"; + +export function PeopleColumn({ + displayPeople, + selectedPerson, + setSelectedPerson, + sortOption, + setSortOption, + getInitials, +}: { + displayPeople: any[]; + selectedPerson: string | null; + setSelectedPerson: (id: string | null) => void; + sortOption: "alphabetical" | "oldest" | "newest"; + setSortOption: (option: "alphabetical" | "oldest" | "newest") => void; + getInitials: (name: string | null) => string; +}) { + return ( +
+
+

People

+
+ + +
+
+
+
+ {displayPeople.map((person: any) => ( + + ))} +
+
+
+ ); +} From e90e34aad81bc289be9c479f9d3d27d392883c73 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 10 Oct 2025 13:04:58 +0900 Subject: [PATCH 6/6] fix typecheck --- apps/desktop2/package.json | 4 +- .../components/main/sidebar/profile/index.tsx | 13 +- pnpm-lock.yaml | 183 +++++++----------- 3 files changed, 85 insertions(+), 115 deletions(-) diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 0228f63a8..4579ae3df 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -14,9 +14,9 @@ "dependencies": { "@electric-sql/client": "^1.0.14", "@hypr/db": "workspace:*", + "@hypr/plugin-analytics": "workspace:*", "@hypr/plugin-db2": "workspace:*", "@hypr/plugin-windows": "workspace:*", - "@hypr/plugin-analytics": "workspace:*", "@hypr/tiptap": "workspace:^", "@hypr/ui": "workspace:^", "@hypr/utils": "workspace:^", @@ -32,6 +32,7 @@ "@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-store": "^2.4.0", "@tauri-apps/plugin-updater": "^2.9.0", + "@wavesurfer/react": "^1.0.11", "@xstate/store": "^3.9.3", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -41,6 +42,7 @@ "react-dom": "^19.2.0", "react-hotkeys-hook": "^4.6.2", "tinybase": "^6.7.0", + "wavesurfer.js": "^7.11.0", "zod": "^4.1.12", "zustand": "^5.0.8" }, diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx index 3640d1432..7bd701ff0 100644 --- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx @@ -49,7 +49,18 @@ export function ProfileSection() { }, [openNew, closeMenu]); const handleClickContacts = useCallback(() => { - openNew({ type: "contacts", active: true }); + openNew({ + type: "contacts", + active: true, + state: { + selectedOrganization: null, + selectedPerson: null, + editingPerson: null, + editingOrg: null, + showNewOrg: false, + sortOption: "alphabetical", + }, + }); closeMenu(); }, [openNew, closeMenu]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32f5ffb4e..4fdf1ab0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,7 +146,7 @@ importers: version: 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-router-devtools': specifier: ^1.132.51 - version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) '@tauri-apps/api': specifier: ^2.8.0 version: 2.8.0 @@ -342,13 +342,13 @@ importers: version: 10.4.21(postcss@8.5.6) eslint: specifier: ^9.37.0 - version: 9.37.0(jiti@1.21.7) + version: 9.37.0(jiti@2.6.1) eslint-plugin-lingui: specifier: ^0.9.0 - version: 0.9.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + version: 0.9.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.37.0(jiti@1.21.7)) + version: 7.37.5(eslint@9.37.0(jiti@2.6.1)) globals: specifier: ^15.15.0 version: 15.15.0 @@ -363,7 +363,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.46.0 - version: 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + version: 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^5.4.20 version: 5.4.20(@types/node@22.18.8)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0) @@ -436,6 +436,9 @@ importers: '@tauri-apps/plugin-updater': specifier: ^2.9.0 version: 2.9.0 + '@wavesurfer/react': + specifier: ^1.0.11 + version: 1.0.11(react@19.2.0)(wavesurfer.js@7.11.0) '@xstate/store': specifier: ^3.9.3 version: 3.9.3(react@19.2.0)(solid-js@1.9.7) @@ -463,6 +466,9 @@ importers: tinybase: specifier: ^6.7.0 version: 6.7.0(postgres@3.4.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(ws@8.18.3) + wavesurfer.js: + specifier: ^7.11.0 + version: 7.11.0 zod: specifier: ^4.1.12 version: 4.1.12 @@ -475,10 +481,10 @@ importers: version: 10.0.0 '@tanstack/react-router-devtools': specifier: ^1.132.51 - version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) '@tanstack/router-plugin': specifier: ^1.132.51 - version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@tauri-apps/cli': specifier: ^2.8.4 version: 2.8.4 @@ -493,7 +499,7 @@ importers: version: 19.2.1(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -508,7 +514,7 @@ importers: version: 5.8.3 vite: specifier: ^7.1.9 - version: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.0)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.10.4(@types/node@24.7.0)(typescript@5.8.3))(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0) @@ -5453,6 +5459,12 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@wavesurfer/react@1.0.11': + resolution: {integrity: sha512-DRpaA3MRTKy4Jby12xvoHASa+w31FZtxaqanXcJjfqNqfamkKi8VJfRnz+Uub9LkpdgoAc3g5SuZF75lEcGgzQ==} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + wavesurfer.js: '>=7.7.14' + '@wdio/cli@8.46.0': resolution: {integrity: sha512-ZT7z4buheFtoXmL8/EPyrspXSwrVRKUI27GLY34hGOjHAhry4dTJ1ODC5ARs0PbuM//yJcJb8q18wa+2xGqf3w==} engines: {node: ^16.13 || >=18} @@ -11855,6 +11867,9 @@ packages: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} + wavesurfer.js@7.11.0: + resolution: {integrity: sha512-LOGdIBIKv/roYuQYClhoqhwbIdQL1GfobLnS2vx0heoLD9lu57OUHWE2DIsCNXBvCsmmbkUvJq9W8bPLPbikGw==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -13332,9 +13347,9 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@2.6.1))': dependencies: - eslint: 9.37.0(jiti@1.21.7) + eslint: 9.37.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -15900,30 +15915,6 @@ snapshots: '@tanstack/query-core': 5.90.2 react: 19.2.0 - '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': - dependencies: - '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.132.51(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - vite: 7.1.9(@types/node@22.18.8)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - '@tanstack/router-core' - - '@types/node' - - csstype - - jiti - - less - - lightningcss - - sass - - sass-embedded - - solid-js - - stylus - - sugarss - - terser - - tiny-invariant - - tsx - - yaml - '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -15948,13 +15939,13 @@ snapshots: - tsx - yaml - '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-router-devtools@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) + '@tanstack/router-devtools-core': 1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@tanstack/router-core' - '@types/node' @@ -16059,29 +16050,6 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': - dependencies: - '@tanstack/router-core': 1.132.47 - clsx: 2.1.1 - goober: 2.1.18(csstype@3.1.3) - solid-js: 1.9.7 - tiny-invariant: 1.3.3 - vite: 7.1.9(@types/node@22.18.8)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - optionalDependencies: - csstype: 3.1.3 - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@22.18.8)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.132.47 @@ -16105,14 +16073,14 @@ snapshots: - tsx - yaml - '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@2.6.1)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/router-devtools-core@1.132.51(@tanstack/router-core@1.132.47)(@types/node@24.7.0)(csstype@3.1.3)(jiti@1.21.7)(lightningcss@1.30.1)(solid-js@1.9.7)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.132.47 clsx: 2.1.1 goober: 2.1.18(csstype@3.1.3) solid-js: 1.9.7 tiny-invariant: 1.3.3 - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: csstype: 3.1.3 transitivePeerDependencies: @@ -16185,7 +16153,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/router-plugin@1.132.51(@tanstack/react-router@1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) @@ -16203,7 +16171,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.132.47(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -16971,15 +16939,15 @@ snapshots: '@types/node': 22.18.8 optional: true - '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.0 - '@typescript-eslint/type-utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.0 - eslint: 9.37.0(jiti@1.21.7) + eslint: 9.37.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -16988,14 +16956,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.46.0 '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 9.37.0(jiti@1.21.7) + eslint: 9.37.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -17018,13 +16986,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 9.37.0(jiti@1.21.7) + eslint: 9.37.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -17048,13 +17016,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.46.0 '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) - eslint: 9.37.0(jiti@1.21.7) + eslint: 9.37.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -17087,7 +17055,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -17095,7 +17063,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -17182,6 +17150,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@wavesurfer/react@1.0.11(react@19.2.0)(wavesurfer.js@7.11.0)': + dependencies: + react: 19.2.0 + wavesurfer.js: 7.11.0 + '@wdio/cli@8.46.0': dependencies: '@types/node': 22.18.8 @@ -19200,16 +19173,16 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-lingui@0.9.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-lingui@0.9.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.37.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) micromatch: 4.0.8 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-react@7.37.5(eslint@9.37.0(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@9.37.0(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -19217,7 +19190,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.37.0(jiti@1.21.7) + eslint: 9.37.0(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -19240,9 +19213,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.37.0(jiti@1.21.7): + eslint@9.37.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.4.0 @@ -19278,7 +19251,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -24606,13 +24579,13 @@ snapshots: typedarray@0.0.6: optional: true - typescript-eslint@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.46.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.37.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -24943,24 +24916,6 @@ snapshots: sugarss: 5.0.1(postcss@8.5.6) terser: 5.44.0 - vite@7.1.9(@types/node@22.18.8)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - esbuild: 0.25.10 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.4 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.18.8 - fsevents: 2.3.3 - jiti: 1.21.7 - lightningcss: 1.30.1 - sugarss: 5.0.1(postcss@8.5.6) - terser: 5.44.0 - tsx: 4.20.6 - yaml: 2.8.1 - vite@7.1.9(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.10 @@ -24979,7 +24934,7 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + vite@7.1.9(@types/node@24.7.0)(jiti@1.21.7)(lightningcss@1.30.1)(sugarss@5.0.1(postcss@8.5.6))(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -24990,7 +24945,7 @@ snapshots: optionalDependencies: '@types/node': 24.7.0 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 1.21.7 lightningcss: 1.30.1 sugarss: 5.0.1(postcss@8.5.6) terser: 5.44.0 @@ -25157,6 +25112,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + wavesurfer.js@7.11.0: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4