Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e74505a
fix:(FI-40): fix conflict
NHSon05 Jun 29, 2026
d15ff38
Merge branch 'dev' and resolve conflicts
NHSon05 Jul 2, 2026
4c0a3b7
Fix Bad Request on update profile and resolve all stash pop conflicts
NHSon05 Jul 2, 2026
93e3c1c
Fix Bad Request (400) by converting null values to empty strings in u…
NHSon05 Jul 2, 2026
479bab8
Sync updated profile data to authStore to prevent old data flash on r…
NHSon05 Jul 2, 2026
e89179e
Fix data unwrap and authStore synchronization in use-query-auth
NHSon05 Jul 2, 2026
475c031
Refactor authentication workflow to strictly keep AccessToken in-memo…
NHSon05 Jul 2, 2026
0a472bf
Fix reload logging out and navigation issues in in-memory auth workflow
NHSon05 Jul 2, 2026
60627fc
Fix ProtectedRoute block by reading AccessToken from in-memory Zustan…
NHSon05 Jul 2, 2026
37dbc60
Restore authentication workflow back to storing AccessToken and Refre…
NHSon05 Jul 2, 2026
ce181ba
Refactor auth: in-memory AT, rt_token key in LS, doubly nested respon…
NHSon05 Jul 2, 2026
42d25d0
Fix automatic logout by using doRefresh() in response interceptor to …
NHSon05 Jul 2, 2026
05bd216
Fix dashboard fullName sync issue by properly unwrapping response dat…
NHSon05 Jul 2, 2026
588a1f3
Commit user manual rollback changes to LocalStorage auth flow
NHSon05 Jul 2, 2026
db2cc20
Manage user profile strictly in-memory using Zustand and React Query,…
NHSon05 Jul 2, 2026
34d951f
Fix reload page blanks by fetching user info on app initialization
NHSon05 Jul 2, 2026
a83790d
Fix reload token loss by only logging out on explicit auth failures (…
NHSon05 Jul 2, 2026
816db88
Correctly unwrap res.data in updateProfile onSuccess mutation handler…
NHSon05 Jul 2, 2026
774642e
Safeguard status mapping in LawyerProfile by checking both lawyerStat…
NHSon05 Jul 2, 2026
c7b9993
Optimize Axios clients to prevent auto-logouts on network failures du…
NHSon05 Jul 2, 2026
97d1ee4
Refactor authentication flow and sync lawyer location field across co…
NHSon05 Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'

import { Agentation } from 'agentation'

import { ThemeProvider } from '@/app/providers/theme-provider'
import AutoScrollToTop from '@/components/scroll/auto-scroll-to-top'
import { scheduleTokenRefresh } from '@/core/shared/auth-refresh'
import { getAccessTokenFromLS } from '@/core/shared/storage'
import { useAuthStore } from '@/core/store/features/auth/authStore'
import useRoutesElements from '@/hooks/routes/use-router-element'

import '@/styles/theme.css'
import { useUserInfo } from './hooks/tanstack-query/auth/use-query-auth'
import { type UserRole } from './models/user/types'

const App = () => {
const AppContent = () => {
const router = useRoutesElements()
return <>{router}</>
}

const App = () => {
const [isAppLoading, setIsAppLoading] = useState(true)
const logout = useAuthStore((state) => state.logout)
const updateUser = useAuthStore((state) => state.updateUser)
const { data: userData } = useUserInfo()

// F5/mở lại tab mà còn token → khởi động timer proactive refresh theo exp.
useEffect(() => {
if (getAccessTokenFromLS()) scheduleTokenRefresh()
}, [])
const initializeApp = async () => {
const accessToken = getAccessTokenFromLS()
if (accessToken) {
try {
updateUser({
userId: userData?.id || '',
fullName: userData?.fullName || '',
email: userData?.email || '',
phone: userData?.phone || '',
location: userData?.location || '',
avatarUrl: userData?.avatarUrl || '',
roleName: userData?.roleName as UserRole
})
scheduleTokenRefresh()
} catch (error: any) {

Check warning on line 42 in frontend/src/App.tsx

View workflow job for this annotation

GitHub Actions / frontend

Unexpected any. Specify a different type
console.error('Failed to fetch user info on app initialize:', error)
if (error.response && [401, 403].includes(error.response.status)) {
logout()
}
}
}
setIsAppLoading(false)
}

initializeApp()
}, [logout, updateUser, userData?.avatarUrl, userData?.email, userData?.fullName, userData?.id, userData?.location, userData?.phone, userData?.roleName])

if (isAppLoading) {
return (
<div className='h-screen w-screen flex flex-col items-center justify-center bg-background-primary'>
<div className='animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent'></div>
<p className='text-sm text-text-description mt-4 font-medium'>Đang khởi tạo ứng dụng...</p>
</div>
)
}

return (
<ThemeProvider>
<AutoScrollToTop behavior='smooth' />
{router}
<AppContent />
{import.meta.env.DEV && <Agentation />}
</ThemeProvider>
)
Expand Down
49 changes: 29 additions & 20 deletions frontend/src/_mocks/lawyer.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.9,
successfulCases: 120,
specializations: ['Doanh nghiệp & Thương mại', 'Lao động'],
city: 'Đà Nẵng'
location: 'Đà Nẵng'
},
{
id: 'lyr-2',
Expand All @@ -23,7 +23,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.8,
successfulCases: 95,
specializations: ['Dân sự & Thừa kế'],
city: 'Hà Nội'
location: 'Hà Nội'
},
{
id: 'lyr-3',
Expand All @@ -34,7 +34,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 5.0,
successfulCases: 210,
specializations: ['Hình sự', 'Hành chính'],
city: 'TP. Hồ Chí Minh'
location: 'TP. Hồ Chí Minh'
},
{
id: 'lyr-4',
Expand All @@ -45,7 +45,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.7,
successfulCases: 65,
specializations: ['Sở hữu trí tuệ', 'Đất đai & Bất động sản'],
city: 'Cần Thơ'
location: 'Cần Thơ'
},
{
id: 'lyr-5',
Expand All @@ -56,7 +56,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.9,
successfulCases: 85,
specializations: ['Doanh nghiệp & Thương mại'],
city: 'Đà Nẵng'
location: 'Đà Nẵng'
},
{
id: 'lyr-6',
Expand All @@ -67,7 +67,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.6,
successfulCases: 42,
specializations: ['Hôn nhân & Gia đình', 'Dân sự & Thừa kế'],
city: 'Hải Phòng'
location: 'Hải Phòng'
},
{
id: 'lyr-7',
Expand All @@ -78,7 +78,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.8,
successfulCases: 67,
specializations: ['Lao động', 'Hành chính'],
city: 'Quảng Nam'
location: 'Quảng Nam'
},
{
id: 'lyr-8',
Expand All @@ -89,7 +89,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.9,
successfulCases: 180,
specializations: ['Đất đai & Bất động sản'],
city: 'Đà Nẵng'
location: 'Đà Nẵng'
},
{
id: 'lyr-9',
Expand All @@ -100,7 +100,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.8,
successfulCases: 140,
specializations: ['Đất đai & Bất động sản', 'Dân sự & Thừa kế'],
city: 'Hà Nội'
location: 'Hà Nội'
},
{
id: 'lyr-10',
Expand All @@ -111,7 +111,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.9,
successfulCases: 165,
specializations: ['Doanh nghiệp & Thương mại'],
city: 'TP. Hồ Chí Minh'
location: 'TP. Hồ Chí Minh'
},
{
id: 'lyr-11',
Expand All @@ -122,7 +122,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.6,
successfulCases: 50,
specializations: ['Sở hữu trí tuệ'],
city: 'Đà Nẵng'
location: 'Đà Nẵng'
},
{
id: 'lyr-12',
Expand All @@ -133,7 +133,7 @@ export const MOCK_LAWYERS: Lawyer[] = [
averageRating: 4.8,
successfulCases: 135,
specializations: ['Hôn nhân & Gia đình'],
city: 'TP. Hồ Chí Minh'
location: 'TP. Hồ Chí Minh'
}
]

Expand All @@ -152,7 +152,7 @@ export const getLawyerListMock = (
size: number = 8,
filters?: {
category?: string
city?: string
location?: string
searchQuery?: string
sortBy?: 'default' | 'rating' | 'cases'
}
Expand All @@ -164,7 +164,7 @@ export const getLawyerListMock = (
filtered = filtered.filter(
(l) =>
l.fullName.toLowerCase().includes(query) ||
l.bio.toLowerCase().includes(query) ||
(l.bio || '').toLowerCase().includes(query) ||
l.specializations.some((s) => s.toLowerCase().includes(query))
)
}
Expand All @@ -174,8 +174,8 @@ export const getLawyerListMock = (
filtered = filtered.filter((l) => l.specializations.includes(category))
}

if (filters?.city && filters.city !== 'Tất cả') {
filtered = filtered.filter((l) => l.city === filters.city)
if (filters?.location && filters.location !== 'Tất cả') {
filtered = filtered.filter((l) => l.location === filters.location)
}

if (filters?.sortBy === 'rating') {
Expand Down Expand Up @@ -205,27 +205,36 @@ export const getLawyerListMock = (
export const getLawyerDetailMock = (id: string): LawyerDetailResponse => {
const lawyer = MOCK_LAWYERS.find((l) => l.id === id) || MOCK_LAWYERS[0]

const expMatch = lawyer.careerHistory.match(/\d+/)
const expMatch = (lawyer.careerHistory || '').match(/\d+/)
const experienceYears = expMatch ? parseInt(expMatch[0], 10) : 5

return {
data: {
items: [],
summary: {
id: lawyer.id,
email: 'lawyer@legalai.vn',
phone: '0987654321',
name: lawyer.fullName,
roleName: 'LAWYER',
bio: lawyer.bio,
averageRating: lawyer.averageRating,
careerHistory: lawyer.careerHistory,
careerMilestones: [],
consultingFee: 500000,
experienceYears,
successfulCases: lawyer.successfulCases || 10,
lawyerStatus: 'AVAILABLE',
status: 'ACTIVE',
licenseInfo: {
isVerified: true,
licenseFileUrl: null,
licenseIssuer: 'Bộ Tư pháp',
licenseNumber: `LS-${lawyer.id.replace('lyr-', '1000')}`
},
name: lawyer.fullName,
role: 'Luật sư thành viên',
specializations: lawyer.specializations
specializations: lawyer.specializations,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/app/layout/layout-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type ReactNode, Suspense } from 'react'

import { Outlet } from 'react-router-dom'

import BottomNav from '@/components/bottom-nav/bottom-nav'
import SideBar from '@/components/side-bar/side-bar'
import TopBar from '@/components/top-bar/top-bar'
import LoadingSpinner from '@/components/ui/loading-spinner'
Expand All @@ -16,7 +17,7 @@ const LayoutMain = ({ children }: LayoutMainProps) => {
<div>
<div className='flex min-h-screen'>
<SideBar />
<div className='flex flex-col flex-1 min-w-0 min-h-0 bg-background-secondary'>
<div className='flex flex-col flex-1 min-w-0 min-h-0 bg-background-secondary pb-20 lg:pb-0 relative'>
<TopBar />
<main
className={cn(
Expand All @@ -40,8 +41,8 @@ const LayoutMain = ({ children }: LayoutMainProps) => {
<div className='mx-auto max-w-none h-full'>
<div
className={cn(
'rounded-2xl border min-h-[calc(100vh-140px)] border-border-secondary',
'shadow-2xl backdrop-blur-xl bg-background-primary',
'sm:rounded-2xl rounded-none sm:border border-none min-h-[calc(100vh-140px)] border-border-secondary',
'shadow-none sm:shadow-2xl backdrop-blur-xl bg-background-primary',
'h-full transition-all duration-300 hover:shadow-3xl',
'mx-auto',
'flex flex-col'
Expand All @@ -58,6 +59,7 @@ const LayoutMain = ({ children }: LayoutMainProps) => {
</div>
</div>
</main>
<BottomNav />
</div>
</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/app/providers/query-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ interface QueryProviderProps {
children: ReactNode
}

const queryClient = new QueryClient({
// eslint-disable-next-line react-refresh/only-export-components
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchIntervalInBackground: false,
Expand All @@ -20,6 +21,15 @@ const queryClient = new QueryClient({
}
})

// Set specific staleTime defaults (5 minutes for userInfo, 3 minutes for lawyers, templates, and laws)
queryClient.setQueryDefaults(['userInfo'], { staleTime: 5 * 60 * 1000 })
queryClient.setQueryDefaults(['lawyers'], { staleTime: 3 * 60 * 1000 })
queryClient.setQueryDefaults(['lawyerDetail'], { staleTime: 3 * 60 * 1000 })
queryClient.setQueryDefaults(['templates'], { staleTime: 3 * 60 * 1000 })
queryClient.setQueryDefaults(['templateDetail'], { staleTime: 3 * 60 * 1000 })
queryClient.setQueryDefaults(['laws'], { staleTime: 3 * 60 * 1000 })
queryClient.setQueryDefaults(['lawDetail'], { staleTime: 3 * 60 * 1000 })

export default function QueryProvider({ children }: QueryProviderProps) {
return (
<QueryClientProvider client={queryClient}>
Expand Down
Loading
Loading