Skip to content

Commit 4d38c3c

Browse files
committed
feat: track last used framework
1 parent 3d1b79f commit 4d38c3c

File tree

9 files changed

+152
-16
lines changed

9 files changed

+152
-16
lines changed

src/auth/auth.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export class AuthService implements IAuthService {
110110
capabilities,
111111
adsDisabled: user.adsDisabled,
112112
interestedInHidingAds: user.interestedInHidingAds,
113+
lastUsedFramework: user.lastUsedFramework,
113114
}
114115
}
115116
}

src/auth/repositories.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export class DrizzleUserRepository implements IUserRepository {
115115
capabilities: user.capabilities as Capability[],
116116
adsDisabled: user.adsDisabled,
117117
interestedInHidingAds: user.interestedInHidingAds,
118+
lastUsedFramework: user.lastUsedFramework,
118119
sessionVersion: user.sessionVersion,
119120
createdAt: user.createdAt,
120121
updatedAt: user.updatedAt,

src/auth/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface AuthUser {
6969
capabilities: Capability[]
7070
adsDisabled: boolean | null
7171
interestedInHidingAds: boolean | null
72+
lastUsedFramework: string | null
7273
}
7374

7475
/**
@@ -83,6 +84,7 @@ export interface DbUser {
8384
capabilities: Capability[]
8485
adsDisabled: boolean | null
8586
interestedInHidingAds: boolean | null
87+
lastUsedFramework: string | null
8688
sessionVersion: number
8789
createdAt: Date
8890
updatedAt: Date
@@ -116,6 +118,7 @@ export interface IUserRepository {
116118
capabilities: Capability[]
117119
adsDisabled: boolean
118120
interestedInHidingAds: boolean
121+
lastUsedFramework: string
119122
sessionVersion: number
120123
updatedAt: Date
121124
}>,

src/components/DocsLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ export function DocsLayout({
343343
{children}
344344
</div>
345345
<AdGate>
346-
<div className="px-2 xl:px-4">
347-
<div className="mb-8 !py-0! mx-auto max-w-full">
346+
<div className="px-2 xl:px-4 flex">
347+
<div className="mb-8 !py-0! mx-auto max-w-full justify-center">
348348
<GamFooter popupPosition="top" />
349349
</div>
350350
</div>

src/components/FrameworkSelect.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useNavigate, useParams } from '@tanstack/react-router'
44
import { Select } from './Select'
55
import { Framework, getLibrary, LibraryId } from '~/libraries'
66
import { getFrameworkOptions } from '~/libraries/frameworks'
7+
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
8+
import { updateLastUsedFramework } from '~/utils/users.server'
79

810
export function FrameworkSelect({ libraryId }: { libraryId: LibraryId }) {
911
const library = getLibrary(libraryId)
@@ -24,7 +26,7 @@ export function FrameworkSelect({ libraryId }: { libraryId: LibraryId }) {
2426
// Let's use zustand to wrap the local storage logic. This way
2527
// we'll get subscriptions for free and we can use it in other
2628
// components if we need to.
27-
const useLocalCurrentFramework = create<{
29+
export const useLocalCurrentFramework = create<{
2830
currentFramework?: string
2931
setCurrentFramework: (framework: string) => void
3032
}>((set) => ({
@@ -38,6 +40,38 @@ const useLocalCurrentFramework = create<{
3840
},
3941
}))
4042

43+
/**
44+
* Get the stored framework preference from localStorage.
45+
* Safe to call during SSR (returns undefined).
46+
*/
47+
export function getStoredFrameworkPreference(): string | undefined {
48+
if (typeof window === 'undefined') return undefined
49+
return localStorage.getItem('framework') || undefined
50+
}
51+
52+
/**
53+
* Hook to persist framework preference.
54+
* Saves to localStorage always, and to DB if user is logged in.
55+
*/
56+
export function usePersistFrameworkPreference() {
57+
const userQuery = useCurrentUserQuery()
58+
const localCurrentFramework = useLocalCurrentFramework()
59+
60+
return React.useCallback(
61+
(framework: string) => {
62+
// Always update localStorage as fallback
63+
localCurrentFramework.setCurrentFramework(framework)
64+
// Update DB for logged-in users (fire-and-forget)
65+
if (userQuery.data) {
66+
updateLastUsedFramework({ data: { framework } }).catch(() => {
67+
// Silently ignore errors - localStorage is the fallback
68+
})
69+
}
70+
},
71+
[localCurrentFramework, userQuery.data],
72+
)
73+
}
74+
4175
function useFrameworkConfig({ frameworks }: { frameworks: Framework[] }) {
4276
const currentFramework = useCurrentFramework(frameworks)
4377

@@ -59,19 +93,24 @@ function useFrameworkConfig({ frameworks }: { frameworks: Framework[] }) {
5993

6094
/**
6195
* Use framework in URL path
96+
* Otherwise use framework from user's DB preference (if logged in)
6297
* Otherwise use framework in localStorage if it exists for this project
6398
* Otherwise fallback to react
6499
*/
65100
export function useCurrentFramework(frameworks: Framework[]) {
66101
const navigate = useNavigate()
102+
const userQuery = useCurrentUserQuery()
67103

68104
const { framework: paramsFramework } = useParams({
69105
strict: false,
70106
})
71107

72108
const localCurrentFramework = useLocalCurrentFramework()
73109

110+
// Priority: URL params > DB (logged-in) > localStorage > 'react'
111+
const userFramework = userQuery.data?.lastUsedFramework
74112
let framework = (paramsFramework ||
113+
userFramework ||
75114
localCurrentFramework.currentFramework ||
76115
'react') as Framework
77116

@@ -82,9 +121,16 @@ export function useCurrentFramework(frameworks: Framework[]) {
82121
navigate({
83122
params: { framework } as any,
84123
})
124+
// Always update localStorage as fallback
85125
localCurrentFramework.setCurrentFramework(framework)
126+
// Update DB for logged-in users (fire-and-forget)
127+
if (userQuery.data) {
128+
updateLastUsedFramework({ data: { framework } }).catch(() => {
129+
// Silently ignore errors - localStorage is the fallback
130+
})
131+
}
86132
},
87-
[localCurrentFramework, navigate],
133+
[localCurrentFramework, navigate, userQuery.data],
88134
)
89135

90136
React.useEffect(() => {

src/components/SearchModal.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { useSearchContext } from '~/contexts/SearchContext'
1717
import { libraries } from '~/libraries'
1818
import { frameworkOptions } from '~/libraries/frameworks'
1919
import { capitalize } from '~/utils/utils'
20+
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
21+
import {
22+
getStoredFrameworkPreference,
23+
usePersistFrameworkPreference,
24+
} from './FrameworkSelect'
2025

2126
function decodeHtmlEntities(str: string): string {
2227
const textarea = document.createElement('textarea')
@@ -87,8 +92,20 @@ function useSearchFilters() {
8792
}
8893

8994
function SearchFiltersProvider({ children }: { children: React.ReactNode }) {
95+
const userQuery = useCurrentUserQuery()
9096
const [selectedLibrary, setSelectedLibrary] = React.useState('')
91-
const [selectedFramework, setSelectedFramework] = React.useState('')
97+
98+
// Get initial framework from user preference (DB if logged in, localStorage otherwise)
99+
const getInitialFramework = React.useCallback(() => {
100+
if (userQuery.data?.lastUsedFramework) {
101+
return userQuery.data.lastUsedFramework
102+
}
103+
return getStoredFrameworkPreference() || ''
104+
}, [userQuery.data?.lastUsedFramework])
105+
106+
const [selectedFramework, setSelectedFramework] = React.useState(
107+
getInitialFramework,
108+
)
92109

93110
const { items: rawLibraryItems, refine: refineLibrary } = useMenu({
94111
attribute: 'library',
@@ -100,6 +117,27 @@ function SearchFiltersProvider({ children }: { children: React.ReactNode }) {
100117
limit: 50,
101118
})
102119

120+
// Pre-filter by stored framework preference on mount (only if no URL framework)
121+
const hasPrefiltered = React.useRef(false)
122+
const pathname = useRouterState({
123+
select: (state) => state.location.pathname,
124+
})
125+
const hasUrlFramework = pathname.includes('/framework/')
126+
127+
React.useEffect(() => {
128+
// Don't pre-filter if URL already specifies a framework (let FrameworkRefinement handle it)
129+
if (hasPrefiltered.current || hasUrlFramework) return
130+
const storedFramework = getInitialFramework()
131+
if (storedFramework && rawFrameworkItems.length > 0) {
132+
const item = rawFrameworkItems.find((i) => i.value === storedFramework)
133+
if (item && !item.isRefined) {
134+
refineFramework(storedFramework)
135+
setSelectedFramework(storedFramework)
136+
hasPrefiltered.current = true
137+
}
138+
}
139+
}, [rawFrameworkItems, refineFramework, getInitialFramework, hasUrlFramework])
140+
103141
// Sort items by their defined order
104142
const libraryItems = [...rawLibraryItems].sort((a, b) => {
105143
const aIndex = libraries.findIndex((l) => l.id === a.value)
@@ -456,6 +494,7 @@ function FrameworkRefinement() {
456494
frameworkItems: items,
457495
} = useSearchFilters()
458496

497+
const persistFramework = usePersistFrameworkPreference()
459498
const hasAutoRefined = React.useRef(false)
460499

461500
// Auto-refine based on current page
@@ -476,6 +515,10 @@ function FrameworkRefinement() {
476515
const handleChange = (value: string) => {
477516
setSelectedFramework(value)
478517
refineFramework(value)
518+
// Persist the framework preference (localStorage + DB if logged in)
519+
if (value) {
520+
persistFramework(value)
521+
}
479522
}
480523

481524
const currentFramework = frameworkOptions.find(
@@ -777,6 +820,8 @@ function SearchResults({ focusedIndex }: { focusedIndex: number }) {
777820
frameworkItems,
778821
} = useSearchFilters()
779822

823+
const persistFramework = usePersistFrameworkPreference()
824+
780825
const algoliaRefinedLibrary =
781826
libraryItems.find((item) => item.isRefined)?.value || null
782827
const algoliaRefinedFramework =
@@ -879,6 +924,7 @@ function SearchResults({ focusedIndex }: { focusedIndex: number }) {
879924
onClick={() => {
880925
setSelectedFramework(fw.value)
881926
refineFramework(fw.value)
927+
persistFramework(fw.value)
882928
}}
883929
className="flex items-center gap-1.5 px-2 py-1 text-xs font-bold rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
884930
>

src/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export const users = pgTable(
9494
capabilities: capabilityEnum('capabilities').array().notNull().default([]),
9595
adsDisabled: boolean('ads_disabled').default(false),
9696
interestedInHidingAds: boolean('interested_in_hiding_ads').default(false),
97+
lastUsedFramework: varchar('last_used_framework', { length: 50 }),
9798
sessionVersion: integer('session_version').notNull().default(0),
9899
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' })
99100
.notNull()

src/routes/$libraryId/$version.docs.framework.$framework.$.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { createFileRoute, useLocation } from '@tanstack/react-router'
1+
import {
2+
createFileRoute,
3+
isNotFound,
4+
redirect,
5+
useLocation,
6+
} from '@tanstack/react-router'
27
import { seo } from '~/utils/seo'
38
import { Doc } from '~/components/Doc'
49
import { loadDocs } from '~/utils/docs'
@@ -10,20 +15,36 @@ export const Route = createFileRoute(
1015
'/$libraryId/$version/docs/framework/$framework/$',
1116
)({
1217
staleTime: 1000 * 60 * 5,
13-
loader: (ctx) => {
18+
loader: async (ctx) => {
1419
const { _splat: docsPath, framework, version, libraryId } = ctx.params
1520

1621
const library = getLibrary(libraryId)
1722

18-
return loadDocs({
19-
repo: library.repo,
20-
branch: getBranch(library, version),
21-
docsPath: `${
22-
library.docsRoot || 'docs'
23-
}/framework/${framework}/${docsPath}`,
24-
currentPath: ctx.location.pathname,
25-
redirectPath: `/${library.id}/${version}/docs/overview`,
26-
})
23+
try {
24+
return await loadDocs({
25+
repo: library.repo,
26+
branch: getBranch(library, version),
27+
docsPath: `${
28+
library.docsRoot || 'docs'
29+
}/framework/${framework}/${docsPath}`,
30+
currentPath: ctx.location.pathname,
31+
redirectPath: `/${library.id}/${version}/docs/overview`,
32+
})
33+
} catch (error) {
34+
// If doc not found, redirect to framework docs root instead of showing 404
35+
// This handles cases like switching frameworks where the same doc path doesn't exist
36+
// Check both isNotFound() and the serialized form from server functions
37+
const isNotFoundError =
38+
isNotFound(error) ||
39+
(error && typeof error === 'object' && 'isNotFound' in error)
40+
if (isNotFoundError) {
41+
throw redirect({
42+
to: '/$libraryId/$version/docs/framework/$framework',
43+
params: { libraryId, version, framework },
44+
})
45+
}
46+
throw error
47+
}
2748
},
2849
component: Docs,
2950
head: (ctx) => {

src/utils/users.server.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,23 @@ export const setInterestedInHidingAds = createServerFn({ method: 'POST' })
341341
return { success: true }
342342
})
343343

344+
// Server function to update user's last used framework preference
345+
export const updateLastUsedFramework = createServerFn({ method: 'POST' })
346+
.inputValidator(z.object({ framework: z.string().min(1).max(50) }))
347+
.handler(async ({ data }) => {
348+
const user = await getAuthenticatedUser()
349+
350+
await db
351+
.update(users)
352+
.set({
353+
lastUsedFramework: data.framework,
354+
updatedAt: new Date(),
355+
})
356+
.where(eq(users.id, user.userId))
357+
358+
return { success: true }
359+
})
360+
344361
// Server function wrapper for updateUserCapabilities (admin only)
345362
export const updateUserCapabilities = createServerFn({ method: 'POST' })
346363
.inputValidator(

0 commit comments

Comments
 (0)