Skip to content

Commit

Permalink
Merge branch 'preview' of github.com:marchhq/march into refac/fe
Browse files Browse the repository at this point in the history
  • Loading branch information
deewakar-k committed Nov 22, 2024
2 parents f4459af + dc002a3 commit 882d462
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 54 deletions.
12 changes: 6 additions & 6 deletions apps/backend/src/controllers/core/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ const authenticateWithGoogleController = async (req, res, next) => {

// Log user event to LogSnag
await logsnag.track({
channel: "waitlist",
event: `${user.userName} is Waitlisted`,
channel: "new-users",
event: `${user.userName} is Added`,
user_id: user._id,
icon: "",
icon: "",
notify: true,
tags: {
method: "Google",
Expand Down Expand Up @@ -83,10 +83,10 @@ const authenticateWithGithubController = async (req, res, next) => {

// Log user event to LogSnag
await logsnag.track({
channel: "waitlist",
event: `${user.userName} is Waitlisted`,
channel: "new-users",
event: `${user.userName} is Added`,
user_id: user._id,
icon: "",
icon: "",
notify: true,
tags: {
method: "Github",
Expand Down
8 changes: 8 additions & 0 deletions apps/backend/src/models/lib/item.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const ItemSchema = new Schema(
title: {
type: String
},
icon: {
type: String,
default: 'home'
},
cover_image: {
type: String,
default: ''
},
type: {
type: String,
default: "issue"
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@fullcalendar/react": "^6.1.15",
"@fullcalendar/timegrid": "^6.1.15",
"@hedgedoc/markdown-it-plugins": "^2.1.4",
"@logsnag/next": "^1.0.3",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.1",
Expand Down Expand Up @@ -50,6 +51,7 @@
"dotenv": "^16.4.5",
"framer-motion": "^11.3.21",
"js-cookies": "^1.0.4",
"lodash": "^4.17.21",
"lucide-react": "^0.439.0",
"markdown-it": "^14.1.0",
"markdown-it-task-lists": "^2.1.1",
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react"

import PageTracker from "@/src/components/PageTracker"
import { Sidebar } from "@/src/components/Sidebar/Sidebar"
import { Toaster } from "@/src/components/ui/toaster"
import { AuthProvider } from "@/src/contexts/AuthContext"
Expand All @@ -15,6 +16,7 @@ const AppLayout: React.FC<Props> = ({ children }) => {
<QueryProvider>
<ModalProvider>
<main className="flex h-screen bg-background">
<PageTracker />
<Sidebar />
<section className="flex-1">{children}</section>
<Toaster />
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/(waitlist)/waitlist/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {}

const Waitlist = (props: Props) => {
return (
<div className="flex flex-col justify-center items-center gap-4 h-screen bg-background text-primary-foreground">
<div className="flex h-screen flex-col items-center justify-center gap-4 bg-background text-primary-foreground">
{/* <Image
src={waitlist}
width={70}
Expand Down
7 changes: 7 additions & 0 deletions apps/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react"

import type { Viewport } from "next"

import { LogSnagProvider } from "@logsnag/next"
import { GoogleOAuthProvider } from "@react-oauth/google"
import { Inter, Source_Serif_4, JetBrains_Mono } from "next/font/google"

Expand Down Expand Up @@ -38,6 +39,12 @@ interface Props {
const RootLayout: React.FC<Props> = ({ children }) => {
return (
<html lang="en">
<head>
<LogSnagProvider
token={process.env.NEXT_PUBLIC_LOGSNAG_TOKEN ?? ""}
project={process.env.NEXT_PUBLIC_LOGSNAG_PROJECT_NAME ?? ""}
/>
</head>
<body
className={classNames(
sansFont.variable,
Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/components/PageTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client"

import { useTrackUserInsights } from "../hooks/useTrackUserInsights"

export default function PageTracker() {
useTrackUserInsights()
return null
}
4 changes: 4 additions & 0 deletions apps/frontend/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
SidebarCollapseProvider,
useSidebarCollapse,
} from "@/src/contexts/SidebarCollapseContext"
import { useTrackPageView } from "@/src/hooks/useTrackPageView"
import { useUserInfo } from "@/src/hooks/useUserInfo"
import classNames from "@/src/utils/classNames"

export const Sidebar: React.FC = () => {
Expand All @@ -26,6 +28,8 @@ export const Sidebar: React.FC = () => {

const SidebarNav: React.FC = () => {
const { isCollapsed } = useSidebarCollapse()
const { user } = useUserInfo()
useTrackPageView(user?.userName || "")
return (
<nav
className={classNames(
Expand Down
26 changes: 26 additions & 0 deletions apps/frontend/src/hooks/useTrackPageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client"

import { useEffect } from "react"

import { useLogSnag } from "@logsnag/next"
import { usePathname } from "next/navigation"

export function useTrackPageView(userId: string | null) {
const { track } = useLogSnag()
const pathname = usePathname()

useEffect(() => {
if (pathname && userId) {
track({
channel: "user-activity",
event: `Page View ${pathname}`,
user_id: userId,
description: `User visited ${pathname}`,
tags: {
page: pathname,
user: userId,
},
})
}
}, [pathname, userId, track])
}
140 changes: 140 additions & 0 deletions apps/frontend/src/hooks/useTrackUserInsights.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client"

import { useEffect, useRef, useCallback, useMemo } from "react"

import { useLogSnag } from "@logsnag/next"
import debounce from "lodash/debounce"
import { usePathname } from "next/navigation"

import { useUserInfo } from "./useUserInfo"
import { useCycleItemStore } from "../lib/store/cycle.store"

export function useTrackUserInsights() {
const { track, identify } = useLogSnag()
const pathname = usePathname()
const { user } = useUserInfo()
const userId = user?.userName || ""
const { inbox } = useCycleItemStore()

const startTimeRef = useRef<number | null>(null)
const totalTimeRef = useRef<number>(0)
const inactivityTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const isActiveRef = useRef(true)
const prevInboxItemsCountRef = useRef(inbox.items.length)

const logTimeSpent = useCallback(async () => {
if (!isActiveRef.current) return

const endTime = Date.now()
if (startTimeRef.current) {
try {
totalTimeRef.current += endTime - startTimeRef.current
startTimeRef.current = null

await track({
channel: "user-activity",
event: "Time Spent",
user_id: userId,
description: `User spent ${Math.round(totalTimeRef.current / 1000)} seconds on ${pathname}`,
tags: {
user: userId || "",
page: pathname,
time_spent: `${Math.round(totalTimeRef.current / 1000)}s`,
},
})
} catch (error) {
console.error("Failed to log time spent:", error)
}
}
}, [track, pathname, userId])

const handleInactivity = useCallback(() => {
if (!isActiveRef.current) return

track({
channel: "user-activity",
event: "Inactivity",
user_id: userId,
description: `User became inactive on ${pathname}`,
tags: { user: userId, page: pathname },
})
}, [track, pathname, userId])

const handleActivity = useMemo(
() =>
debounce(() => {
if (!isActiveRef.current) return

if (inactivityTimeoutRef.current) {
clearTimeout(inactivityTimeoutRef.current)
}
inactivityTimeoutRef.current = setTimeout(handleInactivity, 300000)
}, 150),
[handleInactivity]
)

const handleVisibilityChange = useCallback(() => {
if (!isActiveRef.current) return

if (document.visibilityState === "hidden") {
logTimeSpent()
track({
channel: "user-activity",
event: "Tab Left",
user_id: userId,
description: `User left ${pathname}`,
tags: { user: userId, page: pathname },
})
} else if (document.visibilityState === "visible") {
startTimeRef.current = Date.now()
track({
channel: "user-activity",
event: "Tab Returned",
user_id: userId,
description: `User returned to ${pathname}`,
tags: { user: userId, page: pathname },
})
}
}, [logTimeSpent, track, pathname, userId])

useEffect(() => {
const currentInboxItemsCount = inbox.items.length
if (currentInboxItemsCount !== prevInboxItemsCountRef.current) {
identify({
user_id: userId,
properties: {
inbox_item_count: currentInboxItemsCount,
},
})
prevInboxItemsCountRef.current = currentInboxItemsCount
}
}, [inbox.items, track, userId, pathname])

useEffect(() => {
startTimeRef.current = Date.now()
isActiveRef.current = true

document.addEventListener("visibilitychange", handleVisibilityChange)
window.addEventListener("mousemove", handleActivity)
window.addEventListener("keydown", handleActivity)
window.addEventListener("beforeunload", logTimeSpent)

return () => {
isActiveRef.current = false
document.removeEventListener("visibilitychange", handleVisibilityChange)
window.removeEventListener("mousemove", handleActivity)
window.removeEventListener("keydown", handleActivity)
window.removeEventListener("beforeunload", logTimeSpent)

if (inactivityTimeoutRef.current) {
clearTimeout(inactivityTimeoutRef.current)
}
handleActivity.cancel()
}
}, [handleVisibilityChange, handleActivity, logTimeSpent])

useEffect(() => {
totalTimeRef.current = 0
startTimeRef.current = Date.now()
}, [pathname])
}
16 changes: 15 additions & 1 deletion apps/frontend/src/lib/store/cycle.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,20 @@ export const useCycleItemStore = create<ExtendedCycleItemStore>((set, get) => ({
}
}

if (
updates.status === "done" &&
state.overdue.items.some((item) => item._id === id)
) {
const itemFromOverdue = state.overdue.items.find(
(item) => item._id === id
)
if (itemFromOverdue) {
if (!items.some((item) => item._id === id)) {
return [...items, { ...itemFromOverdue, ...updates }]
}
}
}

// otherwise, just update the item in by date view
return items.map((item) =>
item._id === id ? { ...item, ...updates } : item
Expand Down Expand Up @@ -441,7 +455,7 @@ export const useCycleItemStore = create<ExtendedCycleItemStore>((set, get) => ({
}
}

if (updates.status === "done") {
if (isOverdue && updates.status === "done") {
return items.filter((item) => item._id !== id)
}
}
Expand Down
Loading

0 comments on commit 882d462

Please sign in to comment.