diff --git a/apps/app/tailwind.config.ts b/apps/app/tailwind.config.ts index aa06cb5d86..d2162302c2 100644 --- a/apps/app/tailwind.config.ts +++ b/apps/app/tailwind.config.ts @@ -1,19 +1,15 @@ import Tiptap from "@hypr/tiptap/editor/tailwind.config"; -import UI from "@hypr/ui/tailwind.config"; import typography from "@tailwindcss/typography"; import type { Config } from "tailwindcss"; const config = { - ...UI, content: [ ...Tiptap.content, - ...UI.content, "src/**/*.{js,ts,jsx,tsx}", "index.html", ], theme: { extend: { - ...UI.theme?.extend, fontFamily: { "racing-sans": ["Racing Sans One", "cursive"], }, diff --git a/apps/desktop/src/components/settings-panel/sidebar/extensions-view.tsx b/apps/desktop/src/components/settings-panel/sidebar/extensions-view.tsx index a571105e2d..d2e1922e97 100644 --- a/apps/desktop/src/components/settings-panel/sidebar/extensions-view.tsx +++ b/apps/desktop/src/components/settings-panel/sidebar/extensions-view.tsx @@ -29,7 +29,6 @@ export function ExtensionsView({ const selectedExtension: Extension | null = null; const handleExtensionSelect = (extension: Extension) => { // TODO: Implement extension selection - console.log("Selected extension:", extension); }; return ( diff --git a/apps/desktop/src/components/settings-panel/views/team.tsx b/apps/desktop/src/components/settings-panel/views/team.tsx index 6bac7b847b..eb2ecb43d5 100644 --- a/apps/desktop/src/components/settings-panel/views/team.tsx +++ b/apps/desktop/src/components/settings-panel/views/team.tsx @@ -57,7 +57,6 @@ export default function TeamComponent() { const handleDelete = (member: Member) => { // TODO: Implement delete functionality - console.log("Delete member:", member); }; return ( diff --git a/apps/desktop/src/lib/date.ts b/apps/desktop/src/lib/date.ts index 22c912a23c..d98960f31e 100644 --- a/apps/desktop/src/lib/date.ts +++ b/apps/desktop/src/lib/date.ts @@ -27,8 +27,6 @@ export function formatDateHeader(date: Date): string { const todayStart = startOfToday(); const daysDiff = differenceInCalendarDays(todayStart, date, tzOptions); - console.log(todayStart, date, daysDiff); - if (daysDiff > 1 && daysDiff <= 7) { if (isThisWeek(date, tzOptions)) { return format(date, "EEEE", tzOptions); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index feaa19de54..471b7653eb 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -13,12 +13,12 @@ "dependencies": { "@date-fns/tz": "^1.2.0", "@hookform/resolvers": "^3.10.0", - "@huggingface/languages": "^1.0.0", "@hypr/plugin-auth": "workspace:^", "@hypr/plugin-db": "workspace:^", "@hypr/plugin-listener": "workspace:^", "@hypr/plugin-misc": "workspace:^", "@hypr/plugin-template": "workspace:^", + "@hypr/tiptap": "workspace:^", "@hypr/ui": "workspace:^", "@stackflow/config": "^1.2.1", "@stackflow/core": "^1.2.0", @@ -39,6 +39,7 @@ "zustand": "^5.0.3" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.16", "@tauri-apps/cli": "^2.3.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/apps/mobile/src/components/home/event-item.tsx b/apps/mobile/src/components/home/event-item.tsx new file mode 100644 index 0000000000..1b48ecbfa5 --- /dev/null +++ b/apps/mobile/src/components/home/event-item.tsx @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; +import { formatRemainingTime } from "../../utils/date"; + +import { commands as dbCommands, type Event } from "@hypr/plugin-db"; + +export function EventItem({ event, onSelect }: { event: Event; onSelect: (sessionId: string) => void }) { + const session = useQuery({ + queryKey: ["event-session", event.id], + queryFn: async () => dbCommands.getSession({ calendarEventId: event.id }), + }); + + const handleClick = () => { + if (session.data) { + onSelect(session.data.id); + } + }; + + return ( + + ); +} diff --git a/apps/mobile/src/components/home/index.ts b/apps/mobile/src/components/home/index.ts new file mode 100644 index 0000000000..8f1b0e4246 --- /dev/null +++ b/apps/mobile/src/components/home/index.ts @@ -0,0 +1,2 @@ +export * from "./event-item"; +export * from "./note-item"; diff --git a/apps/mobile/src/components/home/note-item.tsx b/apps/mobile/src/components/home/note-item.tsx new file mode 100644 index 0000000000..805d04ab6e --- /dev/null +++ b/apps/mobile/src/components/home/note-item.tsx @@ -0,0 +1,28 @@ +import { type Session } from "@hypr/plugin-db"; +import { format } from "date-fns"; + +export function NoteItem({ + session, + onSelect, +}: { + session: Session; + onSelect: () => void; +}) { + const sessionDate = new Date(session.created_at); + + return ( + + ); +} diff --git a/apps/mobile/src/components/note/index.ts b/apps/mobile/src/components/note/index.ts new file mode 100644 index 0000000000..e67b4690b5 --- /dev/null +++ b/apps/mobile/src/components/note/index.ts @@ -0,0 +1,2 @@ +export * from "./note-content"; +export * from "./note-info"; diff --git a/apps/mobile/src/components/note/note-content.tsx b/apps/mobile/src/components/note/note-content.tsx new file mode 100644 index 0000000000..52669243e5 --- /dev/null +++ b/apps/mobile/src/components/note/note-content.tsx @@ -0,0 +1,27 @@ +import { useRef } from "react"; + +import { type Session } from "@hypr/plugin-db"; +import Editor, { TiptapEditor } from "@hypr/tiptap/editor"; + +interface ContentProps { + session: Session; +} + +export function NoteContent({ session }: ContentProps) { + const editorRef = useRef<{ editor: TiptapEditor }>(null); + + return ( +
+
+ { + // TODO: implement + }} + initialContent={session.enhanced_memo_html || session.raw_memo_html} + autoFocus={false} + /> +
+
+ ); +} diff --git a/apps/mobile/src/components/note/note-info.tsx b/apps/mobile/src/components/note/note-info.tsx new file mode 100644 index 0000000000..f15803ccc8 --- /dev/null +++ b/apps/mobile/src/components/note/note-info.tsx @@ -0,0 +1,94 @@ +import { Users2Icon } from "lucide-react"; + +import type { Session } from "@hypr/plugin-db"; +import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; + +interface SessionInfoProps { + session: Session; +} + +export function NoteInfo({ session }: SessionInfoProps) { + const hasParticipants = session.conversations.length > 0 + && session.conversations.some((conv) => conv.diarizations.length > 0); + + const participantsCount = hasParticipants + ? session.conversations.flatMap((conv) => conv.diarizations).length + : 0; + + const currentDate = new Date().toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + + const uniqueParticipants = hasParticipants + ? Array.from( + new Set( + session.conversations.flatMap((conv) => conv.diarizations.map((d) => d.speaker)), + ), + ) + : []; + + return ( +
+

+ {session.title || "Untitled"} +

+ +
+
+ {currentDate} +
+ + {hasParticipants && ( +
+
+ + + + + {participantsCount} Participant + {participantsCount !== 1 ? "s" : ""} + + + +
+
+

+ Participants +

+
+
+ {uniqueParticipants.map((participant, index) => ( +
+
+
+
+ + {participant.substring(0, 2).toUpperCase()} + +
+ + {participant} + +
+
+
+ ))} +
+
+
+
+
+ )} +
+
+ ); +} diff --git a/apps/mobile/src/components/recordings/index.ts b/apps/mobile/src/components/recordings/index.ts new file mode 100644 index 0000000000..42789a1e8e --- /dev/null +++ b/apps/mobile/src/components/recordings/index.ts @@ -0,0 +1 @@ +export * from "./recording-item"; diff --git a/apps/mobile/src/components/recordings/recording-item.tsx b/apps/mobile/src/components/recordings/recording-item.tsx new file mode 100644 index 0000000000..46411d8f16 --- /dev/null +++ b/apps/mobile/src/components/recordings/recording-item.tsx @@ -0,0 +1,44 @@ +import { format } from "date-fns"; +import { CheckIcon } from "lucide-react"; +import { type LocalRecording } from "../../mock/recordings"; +import { formatFileSize, formatRecordingDuration } from "../../utils"; + +export const RecordingItem = ({ + recording, + onSelect, + isSelected = false, +}: { + recording: LocalRecording; + onSelect: () => void; + isSelected?: boolean; +}) => { + const recordingDate = new Date(recording.created_at); + + return ( +
+ +
+ ); +}; diff --git a/apps/mobile/src/main.tsx b/apps/mobile/src/main.tsx index c390231917..53fda1ab46 100644 --- a/apps/mobile/src/main.tsx +++ b/apps/mobile/src/main.tsx @@ -5,7 +5,6 @@ import "./styles/globals.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { StrictMode, Suspense } from "react"; import ReactDOM from "react-dom/client"; -import { HyprProvider } from "./contexts/hypr"; import { Stack } from "./stackflow"; const queryClient = new QueryClient(); @@ -17,9 +16,7 @@ if (!rootElement.innerHTML) { - - - + , diff --git a/apps/mobile/src/mock/home.ts b/apps/mobile/src/mock/home.ts new file mode 100644 index 0000000000..7acb052a82 --- /dev/null +++ b/apps/mobile/src/mock/home.ts @@ -0,0 +1,146 @@ +import type { Event, Session } from "@hypr/plugin-db"; + +export const mockSessions: Session[] = [ + { + id: "session-1", + created_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), + visited_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000 + 75 * 60 * 1000).toISOString(), + user_id: "user-123", + calendar_event_id: "event-456", + title: "Weekly Team Standup", + audio_local_path: "/recordings/session-1.mp3", + audio_remote_path: "https://storage.hyprnote.com/user-123/recordings/session-1.mp3", + raw_memo_html: "

Discussed project timeline updates and resource allocation.

", + enhanced_memo_html: + "

Weekly Team Standup

Discussed project timeline updates and resource allocation.

", + conversations: [], + }, + { + id: "session-2", + created_at: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString(), + visited_at: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000 + 90 * 60 * 1000).toISOString(), + user_id: "user-123", + calendar_event_id: null, + title: "Product Strategy Brainstorm", + audio_local_path: "/recordings/session-2.mp3", + audio_remote_path: null, + raw_memo_html: "

Brainstormed new feature ideas for Q3.

", + enhanced_memo_html: + "

Product Strategy Brainstorm

Brainstormed new feature ideas for Q3.

", + conversations: [], + }, + { + id: "session-3", + created_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), + visited_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000).toISOString(), + user_id: "user-123", + calendar_event_id: "event-789", + title: "1:1 with Manager", + audio_local_path: "/recordings/session-3.mp3", + audio_remote_path: "https://storage.hyprnote.com/user-123/recordings/session-3.mp3", + raw_memo_html: "

Quarterly review prep and career development discussion.

", + enhanced_memo_html: + "

1:1 with Manager

Quarterly review prep and career development discussion.

Action Items:

", + conversations: [], + }, + { + id: "session-4", + created_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), + visited_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000 + 75 * 60 * 1000).toISOString(), + user_id: "user-123", + calendar_event_id: null, + title: "Design System Planning", + audio_local_path: "/recordings/session-4.mp3", + audio_remote_path: "https://storage.hyprnote.com/user-123/recordings/session-4.mp3", + raw_memo_html: "

Initial planning for unified design system.

", + enhanced_memo_html: + "

Design System Planning

Initial planning for unified design system.

", + conversations: [], + }, + { + id: "session-5", + created_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), + visited_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000 + 75 * 60 * 1000).toISOString(), + user_id: "user-123", + calendar_event_id: "event-012", + title: "Client Presentation Prep", + audio_local_path: "/recordings/session-5.mp3", + audio_remote_path: null, + raw_memo_html: "

Outline for upcoming client presentation.

", + enhanced_memo_html: + "

Client Presentation Prep

Outline for upcoming client presentation.

  1. Project overview
  2. Timeline updates
  3. Budget considerations
  4. Next steps
", + conversations: [], + }, + { + id: "session-6", + created_at: new Date().toISOString(), + visited_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + user_id: "user-123", + calendar_event_id: "event-345", + title: "Weekly Status Update", + audio_local_path: "/recordings/session-6.mp3", + audio_remote_path: null, + raw_memo_html: "

Weekly progress report with team members.

", + enhanced_memo_html: + "

Weekly Status Update

Weekly progress report with team members.

", + conversations: [], + }, +]; + +export const mockEvents: Event[] = [ + { + id: "event-future-1", + user_id: "user-123", + tracking_id: "track-123", + calendar_id: "cal-123", + name: "Quarterly Planning Meeting", + note: "Prepare Q3 roadmap discussion points", + start_date: new Date(Date.now() + 86400000).toISOString(), + end_date: new Date(Date.now() + 86400000 + 3600000).toISOString(), + google_event_url: "https://calendar.google.com/event?id=abc123", + }, + { + id: "event-future-2", + user_id: "user-123", + tracking_id: "track-456", + calendar_id: "cal-123", + name: "Product Demo with Sales Team", + note: "Show latest feature updates", + start_date: new Date(Date.now() + 172800000).toISOString(), + end_date: new Date(Date.now() + 172800000 + 5400000).toISOString(), + google_event_url: "https://calendar.google.com/event?id=def456", + }, + { + id: "event-future-3", + user_id: "user-123", + tracking_id: "track-789", + calendar_id: "cal-123", + name: "Weekly Team Standup", + note: "Regular team sync", + start_date: new Date(Date.now() + 259200000).toISOString(), + end_date: new Date(Date.now() + 259200000 + 1800000).toISOString(), + google_event_url: "https://calendar.google.com/event?id=ghi789", + }, + { + id: "event-future-4", + user_id: "user-123", + tracking_id: "track-012", + calendar_id: "cal-123", + name: "1:1 with Manager", + note: "Quarterly review prep", + start_date: new Date(Date.now() + 345600000).toISOString(), + end_date: new Date(Date.now() + 345600000 + 2700000).toISOString(), + google_event_url: "https://calendar.google.com/event?id=jkl012", + }, + { + id: "event-future-5", + user_id: "user-123", + tracking_id: "track-345", + calendar_id: "cal-123", + name: "Client Presentation Prep", + note: "Finalize slides", + start_date: new Date(Date.now() + 432000000).toISOString(), + end_date: new Date(Date.now() + 432000000 + 3600000).toISOString(), + google_event_url: null, + }, +]; diff --git a/apps/mobile/src/mock/index.ts b/apps/mobile/src/mock/index.ts new file mode 100644 index 0000000000..cd80bc5e97 --- /dev/null +++ b/apps/mobile/src/mock/index.ts @@ -0,0 +1,2 @@ +export * from "./home"; +export * from "./settings"; diff --git a/apps/mobile/src/mock/recordings.ts b/apps/mobile/src/mock/recordings.ts new file mode 100644 index 0000000000..7b31267a75 --- /dev/null +++ b/apps/mobile/src/mock/recordings.ts @@ -0,0 +1,84 @@ +export interface LocalRecording { + id: string; + filename: string; + title: string; + duration: number; + size: number; + created_at: string; + path: string; +} + +export const localRecordings: LocalRecording[] = [ + { + id: "rec-1", + filename: "recording_20250310_121501.m4a", + title: "Team Meeting Notes", + duration: 1823, + size: 15728640, + created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250310_121501.m4a", + }, + { + id: "rec-2", + filename: "recording_20250309_153022.m4a", + title: "Project Brainstorming", + duration: 2712, + size: 22020096, + created_at: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250309_153022.m4a", + }, + { + id: "rec-3", + filename: "recording_20250308_091534.m4a", + title: "Interview with Client", + duration: 3541, + size: 29360128, + created_at: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250308_091534.m4a", + }, + { + id: "rec-4", + filename: "recording_20250307_143012.m4a", + title: "Personal Notes", + duration: 901, + size: 8388608, + created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250307_143012.m4a", + }, + { + id: "rec-5", + filename: "recording_20250312_103045.m4a", + title: "Quick Idea", + duration: 185, + size: 2097152, + created_at: new Date(Date.now() - 0.3 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250312_103045.m4a", + }, + { + id: "rec-6", + filename: "recording_20250311_163211.m4a", + title: "Research Notes", + duration: 1256, + size: 10485760, + created_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250311_163211.m4a", + }, + { + id: "rec-7", + filename: "recording_20250301_092233.m4a", + title: "Monthly Planning", + duration: 4532, + size: 36700160, + created_at: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250301_092233.m4a", + }, + { + id: "rec-8", + filename: "recording_20250215_143355.m4a", + title: "Product Review", + duration: 2103, + size: 18874368, + created_at: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000).toISOString(), + path: "/storage/emulated/0/Recordings/recording_20250215_143355.m4a", + }, +]; diff --git a/apps/mobile/src/mock/settings.ts b/apps/mobile/src/mock/settings.ts new file mode 100644 index 0000000000..c9df53178e --- /dev/null +++ b/apps/mobile/src/mock/settings.ts @@ -0,0 +1,4 @@ +export const mockUserSettings = { + alertEnhancingDone: true, + remindUpcomingEvents: false, +}; diff --git a/apps/mobile/src/stackflow.config.ts b/apps/mobile/src/stackflow.config.ts index ddcf03a66f..09f3cf96de 100644 --- a/apps/mobile/src/stackflow.config.ts +++ b/apps/mobile/src/stackflow.config.ts @@ -1,31 +1,31 @@ import { defineConfig } from "@stackflow/config"; -import { homeActivityLoader } from "./views/home"; -import { noteActivityLoader } from "./views/note"; -import { profileActivityLoader } from "./views/profile"; -import { settingsActivityLoader } from "./views/settings"; +import { homeLoader } from "./views/home"; +import { noteLoader } from "./views/note"; +import { recordingsLoader } from "./views/recordings"; +import { settingsLoader } from "./views/settings"; export const config = defineConfig({ transitionDuration: 250, activities: [ { - name: "HomeActivity", - loader: homeActivityLoader, + name: "HomeView", + loader: homeLoader, }, { - name: "NoteActivity", - loader: noteActivityLoader, + name: "NoteView", + loader: noteLoader, }, { - name: "LoginActivity", + name: "LoginView", }, { - name: "SettingsActivity", - loader: settingsActivityLoader, + name: "SettingsView", + loader: settingsLoader, }, { - name: "ProfileActivity", - loader: profileActivityLoader, + name: "RecordingsView", + loader: recordingsLoader, }, ], - initialActivity: () => "HomeActivity", + initialActivity: () => "HomeView", }); diff --git a/apps/mobile/src/stackflow.tsx b/apps/mobile/src/stackflow.tsx index 8073b2e2f2..f8bc5dd3a8 100644 --- a/apps/mobile/src/stackflow.tsx +++ b/apps/mobile/src/stackflow.tsx @@ -2,20 +2,20 @@ import { basicUIPlugin } from "@stackflow/plugin-basic-ui"; import { basicRendererPlugin } from "@stackflow/plugin-renderer-basic"; import { stackflow } from "@stackflow/react/future"; import { config } from "./stackflow.config"; -import { HomeActivity } from "./views/home"; -import { LoginActivity } from "./views/login"; -import { NoteActivity } from "./views/note"; -import { ProfileActivity } from "./views/profile"; -import { SettingsActivity } from "./views/settings"; +import { HomeView } from "./views/home"; +import { LoginView } from "./views/login"; +import { NoteView } from "./views/note"; +import { RecordingsView } from "./views/recordings"; +import { SettingsView } from "./views/settings"; export const { Stack } = stackflow({ config, components: { - HomeActivity, - NoteActivity, - LoginActivity, - SettingsActivity, - ProfileActivity, + HomeView, + NoteView, + LoginView, + SettingsView, + RecordingsView, }, plugins: [basicRendererPlugin(), basicUIPlugin({ theme: "cupertino" })], }); diff --git a/apps/mobile/src/utils/date.ts b/apps/mobile/src/utils/date.ts index 22c912a23c..6735f7fdfa 100644 --- a/apps/mobile/src/utils/date.ts +++ b/apps/mobile/src/utils/date.ts @@ -1,6 +1,8 @@ import { tz } from "@date-fns/tz"; import { differenceInCalendarDays, format, isThisWeek, isThisYear, isToday, isYesterday, startOfToday } from "date-fns"; +import { type LocalRecording } from "../mock/recordings"; + import { type Session } from "@hypr/plugin-db"; export type GroupedSessions = Record< @@ -27,8 +29,6 @@ export function formatDateHeader(date: Date): string { const todayStart = startOfToday(); const daysDiff = differenceInCalendarDays(todayStart, date, tzOptions); - console.log(todayStart, date, daysDiff); - if (daysDiff > 1 && daysDiff <= 7) { if (isThisWeek(date, tzOptions)) { return format(date, "EEEE", tzOptions); @@ -63,7 +63,7 @@ export function formatRemainingTime(date: Date): string { } } -export function groupSessionsByDate(sessions: Session[]): GroupedSessions { +export function groupNotesByDate(sessions: Session[]): GroupedSessions { return sessions.reduce((groups, session) => { const date = new Date(session.created_at); const dateKey = format(date, "yyyy-MM-dd"); @@ -80,6 +80,34 @@ export function groupSessionsByDate(sessions: Session[]): GroupedSessions { }, {}); } -export function getSortedDates(groupedSessions: GroupedSessions): string[] { +export function getSortedDatesForNotes(groupedSessions: GroupedSessions): string[] { return Object.keys(groupedSessions).sort((a, b) => b.localeCompare(a)); } + +export const groupRecordingsByDate = (recordings: LocalRecording[]) => { + const groups: Record = {}; + + recordings.forEach(recording => { + const date = new Date(recording.created_at); + const dateKey = date.toISOString().split("T")[0]; + + if (!groups[dateKey]) { + groups[dateKey] = { + date, + recordings: [], + }; + } + + groups[dateKey].recordings.push(recording); + }); + + return groups; +}; + +export const getSortedDatesForRecordings = ( + groupedRecordings: Record, +) => { + return Object.keys(groupedRecordings).sort((a, b) => { + return new Date(b).getTime() - new Date(a).getTime(); + }); +}; diff --git a/apps/mobile/src/utils/file.ts b/apps/mobile/src/utils/file.ts new file mode 100644 index 0000000000..396aae8da1 --- /dev/null +++ b/apps/mobile/src/utils/file.ts @@ -0,0 +1,27 @@ +/** + * Format duration in seconds to mm:ss or hh:mm:ss + * @param seconds Duration in seconds + * @returns Formatted duration string + */ +export const formatRecordingDuration = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`; + } + + return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; +}; + +/** + * Format file size in bytes to human-readable format + * @param bytes File size in bytes + * @returns Formatted file size string (B, KB, MB) + */ +export const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB"; + return (bytes / 1048576).toFixed(1) + " MB"; +}; diff --git a/apps/mobile/src/utils/index.ts b/apps/mobile/src/utils/index.ts new file mode 100644 index 0000000000..ed596f5d61 --- /dev/null +++ b/apps/mobile/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./date"; +export * from "./file"; diff --git a/apps/mobile/src/views/home.tsx b/apps/mobile/src/views/home.tsx index 1445d536b7..3ad5864dfb 100644 --- a/apps/mobile/src/views/home.tsx +++ b/apps/mobile/src/views/home.tsx @@ -1,68 +1,61 @@ import type { ActivityLoaderArgs } from "@stackflow/config"; import { AppScreen } from "@stackflow/plugin-basic-ui"; -import { ActivityComponentType, useFlow } from "@stackflow/react/future"; -import { useQuery } from "@tanstack/react-query"; -import { format, isFuture } from "date-fns"; -import { CalendarIcon, Settings } from "lucide-react"; -import { useHypr } from "../contexts/hypr"; -import { formatDateHeader, formatRemainingTime, getSortedDates, groupSessionsByDate } from "../utils/date"; - -import { commands as dbCommands, type Event, type Session } from "@hypr/plugin-db"; -import { Avatar, AvatarFallback, AvatarImage } from "@hypr/ui/components/ui/avatar"; +import { ActivityComponentType, useFlow, useLoaderData } from "@stackflow/react/future"; +import { + AudioLinesIcon, + CalendarIcon, + ChevronDownIcon, + ChevronRightIcon, + MicIcon, + Settings, + SquarePenIcon, +} from "lucide-react"; +import * as React from "react"; + +import { BottomSheet, BottomSheetContent } from "@hypr/ui/components/ui/bottom-sheet"; +import { EventItem, NoteItem } from "../components/home"; +import { mockEvents, mockSessions } from "../mock"; +import { formatDateHeader, getSortedDatesForNotes, groupNotesByDate } from "../utils/date"; + +import { type Session } from "@hypr/plugin-db"; import { Button } from "@hypr/ui/components/ui/button"; -export function homeActivityLoader({}: ActivityLoaderArgs<"HomeActivity">) { - return {}; +export function homeLoader({}: ActivityLoaderArgs<"HomeView">) { + // TODO: For the upcoming events in mobile, let's just fetch < 1 week + return { + upcomingEvents: mockEvents, + notes: mockSessions, + }; } -export const HomeActivity: ActivityComponentType<"HomeActivity"> = () => { - const { userId } = useHypr(); +export const HomeView: ActivityComponentType<"HomeView"> = () => { + const { upcomingEvents, notes } = useLoaderData(); + const [sheetOpen, setSheetOpen] = React.useState(false); + const [upcomingExpanded, setUpcomingExpanded] = React.useState(true); + const { push } = useFlow(); - const events = useQuery({ - queryKey: ["events"], - queryFn: async () => { - const events = await dbCommands.listEvents(userId); - const upcomingEvents = events.filter((event) => { - return isFuture(new Date(event.start_date)); - }); - return upcomingEvents; - }, - }); - - const sessions = useQuery({ - queryKey: ["sessions"], - queryFn: () => dbCommands.listSessions(null), - }); - - const groupedSessions = groupSessionsByDate(sessions.data ?? []); - const sortedDates = getSortedDates(groupedSessions); + const groupedSessions = groupNotesByDate(notes ?? []); + const sortedDates = getSortedDatesForNotes(groupedSessions); const handleClickNote = (id: string) => { - push("NoteActivity", { id }); + push("NoteView", { id }); }; - const handleClickNew = () => { - push("NoteActivity", { id: "new" }); + const handleUploadFile = () => { + push("RecordingsView", {}); + setSheetOpen(false); }; - const handleClickProfile = () => { - push("ProfileActivity", {}); + const handleStartRecord = () => { + push("NoteView", { id: "new" }); + setSheetOpen(false); }; const handleClickSettings = () => { - push("SettingsActivity", {}); + push("SettingsView", {}); }; - const LeftButton = () => ( - - ); - const RightButton = () => ( + )} -
- +
e.stopPropagation()} + > + + + setSheetOpen(false)} + > + + + + +
); }; -function EventItem({ event, onSelect }: { event: Event; onSelect: (sessionId: string) => void }) { - const session = useQuery({ - queryKey: ["event-session", event.id], - queryFn: async () => dbCommands.getSession({ calendarEventId: event.id }), - }); - - const handleClick = () => { - if (session.data) { - onSelect(session.data.id); - } - }; - - return ( - - ); -} - -function SessionItem({ - session, - onSelect, -}: { - session: Session; - onSelect: () => void; -}) { - const sessionDate = new Date(session.created_at); - - return ( - - ); -} - declare module "@stackflow/config" { interface Register { - HomeActivity: {}; + HomeView: {}; } } diff --git a/apps/mobile/src/views/login.tsx b/apps/mobile/src/views/login.tsx index 1146a21ee4..de595b629a 100644 --- a/apps/mobile/src/views/login.tsx +++ b/apps/mobile/src/views/login.tsx @@ -5,7 +5,7 @@ import { Particles } from "@hypr/ui/components/ui/particles"; import PushableButton from "@hypr/ui/components/ui/pushable-button"; import { TextAnimate } from "@hypr/ui/components/ui/text-animate"; -export const LoginActivity: ActivityComponentType<"LoginActivity"> = () => { +export const LoginView: ActivityComponentType<"LoginView"> = () => { const handleSignIn = () => { // TODO: Need to redirect to https://app.hyprnote.com/auth/connect and wait for a callback }; @@ -53,6 +53,6 @@ export const LoginActivity: ActivityComponentType<"LoginActivity"> = () => { declare module "@stackflow/config" { interface Register { - LoginActivity: {}; + LoginView: {}; } } diff --git a/apps/mobile/src/views/note.tsx b/apps/mobile/src/views/note.tsx index 8cd358bbf2..f073bb86a6 100644 --- a/apps/mobile/src/views/note.tsx +++ b/apps/mobile/src/views/note.tsx @@ -1,29 +1,57 @@ import type { ActivityLoaderArgs } from "@stackflow/config"; import { AppScreen } from "@stackflow/plugin-basic-ui"; import { ActivityComponentType, useLoaderData } from "@stackflow/react/future"; +import { Share2Icon } from "lucide-react"; +import { NoteContent, NoteInfo } from "../components/note"; +import { mockSessions } from "../mock/home"; -export function noteActivityLoader({ +export function noteLoader({ params, -}: ActivityLoaderArgs<"NoteActivity">) { +}: ActivityLoaderArgs<"NoteView">) { const { id } = params; - const session = { id }; + // Find the session in the mock data or return a default session + const session = mockSessions.find(s => s.id === id) || { + id, + title: "Untitled Note", + created_at: new Date().toISOString(), + visited_at: new Date().toISOString(), + user_id: "user-123", + calendar_event_id: null, + audio_local_path: null, + audio_remote_path: null, + raw_memo_html: "

No content available.

", + enhanced_memo_html: "

Untitled Note

No content available.

", + conversations: [], + }; return { session }; } -export const NoteActivity: ActivityComponentType<"NoteActivity"> = () => { - const { session } = useLoaderData(); +export const NoteView: ActivityComponentType<"NoteView"> = () => { + const { session } = useLoaderData(); + + const handleShareNote = () => { + // TODO: Implementation for sharing the note would go here + }; + + const ShareButton = () => ( + + ); return ( -
-

NoteActivity

-

{JSON.stringify(session)}

+
+
+ + +
); @@ -31,7 +59,7 @@ export const NoteActivity: ActivityComponentType<"NoteActivity"> = () => { declare module "@stackflow/config" { interface Register { - NoteActivity: { + NoteView: { id: string; }; } diff --git a/apps/mobile/src/views/profile.tsx b/apps/mobile/src/views/profile.tsx deleted file mode 100644 index 12a7f726d9..0000000000 --- a/apps/mobile/src/views/profile.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import type { ActivityLoaderArgs } from "@stackflow/config"; -import { AppScreen } from "@stackflow/plugin-basic-ui"; -import { ActivityComponentType } from "@stackflow/react/future"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { commands as dbCommands, type Human, type Organization } from "@hypr/plugin-db"; -import { Avatar, AvatarFallback, AvatarImage } from "@hypr/ui/components/ui/avatar"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@hypr/ui/components/ui/form"; -import { Input } from "@hypr/ui/components/ui/input"; -import { Textarea } from "@hypr/ui/components/ui/textarea"; - -const schema = z.object({ - fullName: z.string().min(2).max(50).optional(), - jobTitle: z.string().min(2).max(50).optional(), - companyName: z.string().min(2).max(50), - companyDescription: z.string().min(2).max(500).optional(), - linkedinUserName: z.string().min(2).max(50).optional(), -}); - -type Schema = z.infer; - -type ConfigData = { - human: Human | null; - organization: Organization; -}; - -export function profileActivityLoader({}: ActivityLoaderArgs<"ProfileActivity">) { - return {}; -} - -export const ProfileActivity: ActivityComponentType<"ProfileActivity"> = () => { - const queryClient = useQueryClient(); - - const config = useQuery({ - queryKey: ["config", "profile"], - queryFn: async () => { - const [human, organization] = await Promise.all([ - dbCommands.getSelfHuman(), - dbCommands.getSelfOrganization(), - ]); - return { human, organization }; - }, - }); - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - fullName: config.data?.human?.full_name ?? undefined, - jobTitle: config.data?.human?.job_title ?? undefined, - companyName: config.data?.organization.name ?? undefined, - companyDescription: config.data?.organization.description ?? undefined, - linkedinUserName: config.data?.human?.linkedin_username ?? undefined, - }, - }); - - const mutation = useMutation({ - mutationFn: async (v: Schema) => { - if (!config.data) { - console.error("cannot mutate profile because it is not loaded"); - return; - } - - const newHuman: Human = { - ...config.data.human!, - full_name: v.fullName ?? null, - job_title: v.jobTitle ?? null, - email: config.data.human?.email ?? null, - linkedin_username: v.linkedinUserName ?? null, - }; - - const newOrganization: Organization = { - ...config.data.organization, - name: v.companyName, - description: v.companyDescription ?? null, - }; - - try { - await dbCommands.upsertHuman(newHuman); - await dbCommands.upsertOrganization(newOrganization); - } catch (error) { - console.error("error upserting human or organization", error); - } - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["config", "profile"] }); - }, - }); - - useEffect(() => { - const subscription = form.watch(() => form.handleSubmit((v) => mutation.mutate(v))()); - return () => subscription.unsubscribe(); - }, [mutation]); - - const getInitials = () => { - const name = config.data?.human?.full_name ?? ""; - if (!name) return "?"; - return name.split(" ").map(part => part[0]?.toUpperCase() || "").join("").slice(0, 2); - }; - - return ( - -
- - - {getInitials()} - - - {config.isLoading ?
Loading profile data...
: ( -
- - ( - - Full Name - - - - - - )} - /> - - ( - - Job Title - - - - - - )} - /> - - ( - - Company Name - - This is the name of the company you work for. - - - - - - - )} - /> - - ( - - Company Description - - This is a short description of your company. - - -