Skip to content

Commit b23d2f8

Browse files
committed
fix menu interactions
1 parent b0e13d7 commit b23d2f8

File tree

14 files changed

+1853
-1065
lines changed

14 files changed

+1853
-1065
lines changed

src/components/DocsLayout.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ export function DocsLayout({
536536
'sticky top-[var(--navbar-height)] h-[calc(100dvh-var(--navbar-height))]',
537537
'z-10 border-r border-gray-500/20',
538538
'bg-white/50 dark:bg-black/30',
539-
'w-14',
539+
'w-10',
540540
)}
541541
>
542542
<DocsMenuStrip
@@ -583,12 +583,14 @@ export function DocsLayout({
583583
!showLargeMenu && 'sm:-translate-x-full xl:translate-x-0',
584584
showLargeMenu && 'sm:translate-x-0',
585585
)}
586-
onPointerEnter={() => {
586+
onPointerEnter={(e) => {
587+
if (e.pointerType === 'touch') return
587588
if (window.innerWidth < 1280) {
588589
clearTimeout(leaveTimer.current)
589590
}
590591
}}
591-
onPointerLeave={() => {
592+
onPointerLeave={(e) => {
593+
if (e.pointerType === 'touch') return
592594
if (window.innerWidth < 1280) {
593595
leaveTimer.current = setTimeout(() => {
594596
setShowLargeMenu(false)

src/components/Navbar.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,22 @@ export function Navbar({ children }: { children: React.ReactNode }) {
8282
}, [])
8383

8484
const [showMenu, setShowMenu] = React.useState(false)
85-
const isPointerInsideButtonRef = React.useRef(false)
86-
87-
const toggleMenu = () => {
88-
setShowMenu((prev) => !prev)
89-
}
85+
const pointerInsideButtonRef = React.useRef(false)
9086

9187
const largeMenuRef = React.useRef<HTMLDivElement>(null)
88+
const menuButtonRef = React.useRef<HTMLButtonElement>(null)
9289

9390
// Close mobile menu when clicking outside
9491
const smallMenuRef = useClickOutside<HTMLDivElement>({
9592
enabled: showMenu,
9693
onClickOutside: () => setShowMenu(false),
97-
additionalRefs: [largeMenuRef],
94+
additionalRefs: [largeMenuRef, menuButtonRef],
9895
})
9996

97+
const toggleMenu = () => {
98+
setShowMenu((prev) => !prev)
99+
}
100+
100101
const loginButton = (
101102
<>
102103
{(() => {
@@ -202,19 +203,22 @@ export function Navbar({ children }: { children: React.ReactNode }) {
202203
? 'lg:w-9 lg:opacity-100 lg:translate-x-0'
203204
: 'lg:w-0 lg:opacity-0 lg:-translate-x-full',
204205
)}
206+
ref={menuButtonRef}
205207
onClick={toggleMenu}
206-
onPointerEnter={() => {
207-
if (window.innerWidth < 1024) {
208+
onPointerEnter={(e) => {
209+
// Only open on hover for desktop pointer devices
210+
if (window.innerWidth < 1024 || e.pointerType === 'touch') {
208211
return
209212
}
210-
if (isPointerInsideButtonRef.current) {
213+
// Don't reopen if pointer is already inside (e.g. after clicking X)
214+
if (pointerInsideButtonRef.current) {
211215
return
212216
}
213-
isPointerInsideButtonRef.current = true
217+
pointerInsideButtonRef.current = true
214218
setShowMenu(true)
215219
}}
216220
onPointerLeave={() => {
217-
isPointerInsideButtonRef.current = false
221+
pointerInsideButtonRef.current = false
218222
}}
219223
>
220224
{showMenu ? <X /> : <Menu />}
@@ -649,16 +653,18 @@ export function Navbar({ children }: { children: React.ReactNode }) {
649653
!inlineMenu && !showMenu && '-translate-x-full',
650654
!inlineMenu && showMenu && 'translate-x-0',
651655
)}
652-
onPointerEnter={() => {
656+
onPointerEnter={(e) => {
657+
if (e.pointerType === 'touch') return
653658
clearTimeout(leaveTimer.current)
654659
}}
655-
onPointerLeave={() => {
660+
onPointerLeave={(e) => {
661+
if (e.pointerType === 'touch') return
656662
leaveTimer.current = setTimeout(() => {
657663
setShowMenu(false)
658664
}, 300)
659665
}}
660666
>
661-
<div className="flex-1 flex flex-col gap-4 whitespace-nowrap overflow-y-auto text-base pb-[50px] min-w-[260px]">
667+
<div className="flex-1 flex flex-col gap-4 whitespace-nowrap overflow-y-auto text-base pb-[50px] min-w-[220px]">
662668
<div className="flex flex-col gap-1 text-sm p-2">{items}</div>
663669
</div>
664670
</div>

src/db/schema.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
real,
1313
index,
1414
uniqueIndex,
15+
date,
1516
} from 'drizzle-orm/pg-core'
1617
import { relations } from 'drizzle-orm'
1718
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'
@@ -772,6 +773,32 @@ export const auditLogs = pgTable(
772773
export type AuditLog = InferSelectModel<typeof auditLogs>
773774
export type NewAuditLog = InferInsertModel<typeof auditLogs>
774775

776+
// Daily user activity table (one row per user per day for DAU/streak tracking)
777+
export const userActivity = pgTable(
778+
'user_activity',
779+
{
780+
id: uuid('id').primaryKey().defaultRandom(),
781+
userId: uuid('user_id')
782+
.notNull()
783+
.references(() => users.id, { onDelete: 'cascade' }),
784+
date: date('date', { mode: 'string' }).notNull(), // YYYY-MM-DD format
785+
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' })
786+
.notNull()
787+
.defaultNow(),
788+
},
789+
(table) => ({
790+
userDateUnique: uniqueIndex('user_activity_user_date_unique').on(
791+
table.userId,
792+
table.date,
793+
),
794+
userIdIdx: index('user_activity_user_id_idx').on(table.userId),
795+
dateIdx: index('user_activity_date_idx').on(table.date),
796+
}),
797+
)
798+
799+
export type UserActivity = InferSelectModel<typeof userActivity>
800+
export type NewUserActivity = InferInsertModel<typeof userActivity>
801+
775802
// Relations
776803
export const usersRelations = relations(users, ({ many }) => ({
777804
sessions: many(sessions),
@@ -782,6 +809,7 @@ export const usersRelations = relations(users, ({ many }) => ({
782809
bannerDismissals: many(bannerDismissals),
783810
loginHistory: many(loginHistory),
784811
auditLogs: many(auditLogs),
812+
userActivity: many(userActivity),
785813
}))
786814

787815
export const rolesRelations = relations(roles, ({ many }) => ({
@@ -868,3 +896,10 @@ export const auditLogsRelations = relations(auditLogs, ({ one }) => ({
868896
references: [users.id],
869897
}),
870898
}))
899+
900+
export const userActivityRelations = relations(userActivity, ({ one }) => ({
901+
user: one(users, {
902+
fields: [userActivity.userId],
903+
references: [users.id],
904+
}),
905+
}))

src/hooks/useClickOutside.ts

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ export function useClickOutside<T extends HTMLElement = HTMLElement>({
3232
additionalRefs = [],
3333
}: UseClickOutsideOptions): React.RefObject<T | null> {
3434
const ref = React.useRef<T>(null)
35-
const touchStartRef = React.useRef<{ x: number; y: number } | null>(null)
35+
const touchStartRef = React.useRef<{
36+
x: number
37+
y: number
38+
outside: boolean
39+
} | null>(null)
3640

3741
React.useEffect(() => {
3842
if (!enabled) return
@@ -45,39 +49,48 @@ export function useClickOutside<T extends HTMLElement = HTMLElement>({
4549
return false
4650
}
4751

48-
const handlePointerDown = (event: PointerEvent) => {
49-
if (event.pointerType === 'touch') {
50-
// Track touch start position to detect scrolls vs taps
51-
touchStartRef.current = { x: event.clientX, y: event.clientY }
52-
return
53-
}
52+
// Mouse: handle on mousedown for immediate response
53+
const handleMouseDown = (event: MouseEvent) => {
54+
// If this mousedown was generated from a touch, ignore it
55+
if ((event as any).sourceCapabilities?.firesTouchEvents) return
5456

55-
// Mouse/pen: handle immediately
5657
if (!isInsideRefs(event.target as Node)) {
5758
onClickOutside()
5859
}
5960
}
6061

61-
const handlePointerUp = (event: PointerEvent) => {
62-
if (event.pointerType !== 'touch') return
62+
// Touch: only close if tap started AND ended outside
63+
const handleTouchStart = (event: TouchEvent) => {
64+
const touch = event.touches[0]
65+
if (touch) {
66+
const target = event.target as Node
67+
const startedOutside = !isInsideRefs(target)
68+
touchStartRef.current = {
69+
x: touch.clientX,
70+
y: touch.clientY,
71+
outside: startedOutside,
72+
}
73+
}
74+
}
6375

76+
const handleTouchEnd = (event: TouchEvent) => {
6477
const start = touchStartRef.current
6578
touchStartRef.current = null
6679

6780
if (!start) return
6881

82+
// Only consider closing if touch started outside
83+
if (!start.outside) return
84+
85+
const touch = event.changedTouches[0]
86+
if (!touch) return
87+
6988
// If finger moved more than 10px, it's a scroll not a tap
70-
const dx = Math.abs(event.clientX - start.x)
71-
const dy = Math.abs(event.clientY - start.y)
89+
const dx = Math.abs(touch.clientX - start.x)
90+
const dy = Math.abs(touch.clientY - start.y)
7291
if (dx > 10 || dy > 10) return
7392

74-
if (!isInsideRefs(event.target as Node)) {
75-
onClickOutside()
76-
}
77-
}
78-
79-
const handlePointerCancel = () => {
80-
touchStartRef.current = null
93+
onClickOutside()
8194
}
8295

8396
const handleEscape = (event: KeyboardEvent) => {
@@ -86,17 +99,17 @@ export function useClickOutside<T extends HTMLElement = HTMLElement>({
8699
}
87100
}
88101

89-
document.addEventListener('pointerdown', handlePointerDown)
90-
document.addEventListener('pointerup', handlePointerUp)
91-
document.addEventListener('pointercancel', handlePointerCancel)
102+
document.addEventListener('mousedown', handleMouseDown)
103+
document.addEventListener('touchstart', handleTouchStart, { passive: true })
104+
document.addEventListener('touchend', handleTouchEnd)
92105
if (closeOnEscape) {
93106
document.addEventListener('keydown', handleEscape)
94107
}
95108

96109
return () => {
97-
document.removeEventListener('pointerdown', handlePointerDown)
98-
document.removeEventListener('pointerup', handlePointerUp)
99-
document.removeEventListener('pointercancel', handlePointerCancel)
110+
document.removeEventListener('mousedown', handleMouseDown)
111+
document.removeEventListener('touchstart', handleTouchStart)
112+
document.removeEventListener('touchend', handleTouchEnd)
100113
if (closeOnEscape) {
101114
document.removeEventListener('keydown', handleEscape)
102115
}

src/routeTree.gen.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { Route as LibrariesIndexRouteImport } from './routes/_libraries/index'
2424
import { Route as LibraryIdIndexRouteImport } from './routes/$libraryId/index'
2525
import { Route as AuthSignoutRouteImport } from './routes/auth/signout'
2626
import { Route as AdminUsersRouteImport } from './routes/admin/users'
27-
import { Route as AdminStatsRouteImport } from './routes/admin/stats'
2827
import { Route as AdminNpmStatsRouteImport } from './routes/admin/npm-stats'
2928
import { Route as AdminLoginsRouteImport } from './routes/admin/logins'
3029
import { Route as AdminGithubStatsRouteImport } from './routes/admin/github-stats'
@@ -167,11 +166,6 @@ const AdminUsersRoute = AdminUsersRouteImport.update({
167166
path: '/users',
168167
getParentRoute: () => AdminRouteRoute,
169168
} as any)
170-
const AdminStatsRoute = AdminStatsRouteImport.update({
171-
id: '/stats',
172-
path: '/stats',
173-
getParentRoute: () => AdminRouteRoute,
174-
} as any)
175169
const AdminNpmStatsRoute = AdminNpmStatsRouteImport.update({
176170
id: '/npm-stats',
177171
path: '/npm-stats',
@@ -562,7 +556,6 @@ export interface FileRoutesByFullPath {
562556
'/admin/github-stats': typeof AdminGithubStatsRoute
563557
'/admin/logins': typeof AdminLoginsRoute
564558
'/admin/npm-stats': typeof AdminNpmStatsRoute
565-
'/admin/stats': typeof AdminStatsRoute
566559
'/admin/users': typeof AdminUsersRoute
567560
'/auth/signout': typeof AuthSignoutRoute
568561
'/$libraryId/': typeof LibraryIdIndexRoute
@@ -642,7 +635,6 @@ export interface FileRoutesByTo {
642635
'/admin/github-stats': typeof AdminGithubStatsRoute
643636
'/admin/logins': typeof AdminLoginsRoute
644637
'/admin/npm-stats': typeof AdminNpmStatsRoute
645-
'/admin/stats': typeof AdminStatsRoute
646638
'/admin/users': typeof AdminUsersRoute
647639
'/auth/signout': typeof AuthSignoutRoute
648640
'/$libraryId': typeof LibraryIdIndexRoute
@@ -727,7 +719,6 @@ export interface FileRoutesById {
727719
'/admin/github-stats': typeof AdminGithubStatsRoute
728720
'/admin/logins': typeof AdminLoginsRoute
729721
'/admin/npm-stats': typeof AdminNpmStatsRoute
730-
'/admin/stats': typeof AdminStatsRoute
731722
'/admin/users': typeof AdminUsersRoute
732723
'/auth/signout': typeof AuthSignoutRoute
733724
'/$libraryId/': typeof LibraryIdIndexRoute
@@ -813,7 +804,6 @@ export interface FileRouteTypes {
813804
| '/admin/github-stats'
814805
| '/admin/logins'
815806
| '/admin/npm-stats'
816-
| '/admin/stats'
817807
| '/admin/users'
818808
| '/auth/signout'
819809
| '/$libraryId/'
@@ -893,7 +883,6 @@ export interface FileRouteTypes {
893883
| '/admin/github-stats'
894884
| '/admin/logins'
895885
| '/admin/npm-stats'
896-
| '/admin/stats'
897886
| '/admin/users'
898887
| '/auth/signout'
899888
| '/$libraryId'
@@ -977,7 +966,6 @@ export interface FileRouteTypes {
977966
| '/admin/github-stats'
978967
| '/admin/logins'
979968
| '/admin/npm-stats'
980-
| '/admin/stats'
981969
| '/admin/users'
982970
| '/auth/signout'
983971
| '/$libraryId/'
@@ -1158,13 +1146,6 @@ declare module '@tanstack/react-router' {
11581146
preLoaderRoute: typeof AdminUsersRouteImport
11591147
parentRoute: typeof AdminRouteRoute
11601148
}
1161-
'/admin/stats': {
1162-
id: '/admin/stats'
1163-
path: '/stats'
1164-
fullPath: '/admin/stats'
1165-
preLoaderRoute: typeof AdminStatsRouteImport
1166-
parentRoute: typeof AdminRouteRoute
1167-
}
11681149
'/admin/npm-stats': {
11691150
id: '/admin/npm-stats'
11701151
path: '/npm-stats'
@@ -1808,7 +1789,6 @@ interface AdminRouteRouteChildren {
18081789
AdminGithubStatsRoute: typeof AdminGithubStatsRoute
18091790
AdminLoginsRoute: typeof AdminLoginsRoute
18101791
AdminNpmStatsRoute: typeof AdminNpmStatsRoute
1811-
AdminStatsRoute: typeof AdminStatsRoute
18121792
AdminUsersRoute: typeof AdminUsersRoute
18131793
AdminIndexRoute: typeof AdminIndexRoute
18141794
AdminBannersIdRoute: typeof AdminBannersIdRoute
@@ -1826,7 +1806,6 @@ const AdminRouteRouteChildren: AdminRouteRouteChildren = {
18261806
AdminGithubStatsRoute: AdminGithubStatsRoute,
18271807
AdminLoginsRoute: AdminLoginsRoute,
18281808
AdminNpmStatsRoute: AdminNpmStatsRoute,
1829-
AdminStatsRoute: AdminStatsRoute,
18301809
AdminUsersRoute: AdminUsersRoute,
18311810
AdminIndexRoute: AdminIndexRoute,
18321811
AdminBannersIdRoute: AdminBannersIdRoute,

0 commit comments

Comments
 (0)