-
-
Try Hyprnote Pro
+
+
+ Hyprnote Pro
-
-
- Experience smarter meetings with free trial.
+
+ Free trial for smarter meetings
-
+
+
);
}
diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx
index 1543cf049..7bd701ff0 100644
--- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx
+++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx
@@ -49,9 +49,20 @@ export function ProfileSection() {
}, [openNew, closeMenu]);
const handleClickContacts = useCallback(() => {
- console.log("Contacts");
+ openNew({
+ type: "contacts",
+ active: true,
+ state: {
+ selectedOrganization: null,
+ selectedPerson: null,
+ editingPerson: null,
+ editingOrg: null,
+ showNewOrg: false,
+ sortOption: "alphabetical",
+ },
+ });
closeMenu();
- }, [closeMenu]);
+ }, [openNew, closeMenu]);
const handleClickDailyNote = useCallback(() => {
console.log("Daily note");
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 a481d9c35..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],
)!;
@@ -463,6 +492,8 @@ export const StoreComponent = () => {
export const QUERIES = {
eventsWithoutSession: "eventsWithoutSession",
sessionsWithMaybeEvent: "sessionsWithMaybeEvent",
+ visibleOrganizations: "visibleOrganizations",
+ visibleHumans: "visibleHumans",
};
export const METRICS = {
diff --git a/apps/desktop2/src/store/zustand/tabs.ts b/apps/desktop2/src/store/zustand/tabs.ts
index e1c299bbc..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(),
@@ -125,14 +162,15 @@ 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("calendars"),
+ month: z.coerce.date(),
+ }),
]);
export type Tab = z.infer;
@@ -148,6 +186,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 +208,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"}`;
}
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: {
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";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 024ebb361..4fdf1ab0f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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)
@@ -430,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)
@@ -457,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
@@ -5447,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}
@@ -11849,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==}
@@ -17129,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
@@ -25086,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