Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion src/app/(public)/about/components/AboutBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box, Container } from '@mui/material';
import { styled } from '@mui/material/styles';
import { minWidth } from '@mui/system';

export const AboutHeader = styled(Box)(({ theme }) => ({
display: 'flex',
Expand Down
2 changes: 2 additions & 0 deletions src/app/(public)/blogs/components/BlogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default function BlogList() {
setBlogs(fetched);
setTotal(res.data.total);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to fetch blogs:', err);
}
};
Expand Down Expand Up @@ -96,6 +97,7 @@ export default function BlogList() {
scroll: true,
});
} catch (e) {
// eslint-disable-next-line no-console
console.error('check next page failed', e);
}
};
Expand Down
6 changes: 3 additions & 3 deletions src/app/(public)/login/component/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@ export default function LoginForm() {

const [loginUser, { isLoading, error }] = useLoginUserMutation();
const [showPassword, setShowPassword] = useState(false);
const token = useAppSelector(s => s.auth.token);
const isAuthenticated = useAppSelector(s => s.auth.isAuthenticated);
const router = useRouter();

useEffect(() => {
if (token) {
if (isAuthenticated) {
router.replace('/admin/overview');
}
}, [token, router]);
}, [isAuthenticated, router]);

const onSubmit = async (data: LoginFormData) => {
await loginUser({ email: data.workEmail, password: data.password });
Expand Down
2 changes: 1 addition & 1 deletion src/app/(public)/pricing/components/PricingSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function PricingSection() {
(a, b) => tierOrder[a.tier] - tierOrder[b.tier],
);

const [currentSlide, setCurrentSlide] = useState(0);
const [, setCurrentSlide] = useState(0);
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
slides: {
perView: 1,
Expand Down
82 changes: 68 additions & 14 deletions src/app/(public)/reduxtest/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,33 @@
import { useEffect } from 'react';

import { logout } from '@/features/auth/authSlice';
import { useCSRFToken } from '@/features/auth/hooks/useCSRFToken';
import { useLazyGetUnauthorizedQuery } from '@/features/test/testApiSlice';
import { useAppDispatch, useAppSelector } from '@/redux/hooks';

export default function ReduxTestPage() {
const dispatch = useAppDispatch();

const token = useAppSelector(state => state.auth.token);
const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated);
const user = useAppSelector(state => state.auth.user);
const csrfToken = useAppSelector(state => state.auth.csrfToken);

const { isTokenValid, refreshToken, getToken } = useCSRFToken();

useEffect(() => {
// eslint-disable-next-line no-console
console.groupCollapsed('🔑 Auth State');
// eslint-disable-next-line no-console
console.log('Token:', token);
console.log('Authenticated:', isAuthenticated);
// eslint-disable-next-line no-console
console.log('User:', user);
// eslint-disable-next-line no-console
console.log('CSRF Token:', csrfToken);
// eslint-disable-next-line no-console
console.log('CSRF Token Valid:', isTokenValid());
// eslint-disable-next-line no-console
console.groupEnd();
}, [token, user]);
}, [isAuthenticated, user, csrfToken, isTokenValid]);

const [triggerUnauthorized, { isFetching, error }] =
useLazyGetUnauthorizedQuery();
Expand All @@ -35,23 +43,69 @@ export default function ReduxTestPage() {
}
};

const handleRefreshCSRF = async () => {
try {
const success = await refreshToken();
// eslint-disable-next-line no-console
console.log('🔄 CSRF Token Refresh:', success ? 'Success' : 'Failed');
} catch (err) {
// eslint-disable-next-line no-console
console.error('❌ CSRF Refresh Error:', err);
}
};

const handleGetCSRF = async () => {
try {
const token = await getToken();
// eslint-disable-next-line no-console
console.log('📋 Get CSRF Token:', token ? 'Success' : 'Failed');
} catch (err) {
// eslint-disable-next-line no-console
console.error('❌ Get CSRF Error:', err);
}
};

return (
<main style={{ padding: 40 }}>
<h1>Redux Key State Test (see console)</h1>
<p>login success, see token and user info in console</p>
<button onClick={() => dispatch(logout())} style={{ marginTop: 24 }}>
Clear Auth State (redux)
</button>

<button
onClick={() => void handleTrigger401()}
style={{ marginTop: 24, marginLeft: 16 }}
disabled={isFetching}
>
Simulate 401 Error (axiosBaseQuery + redux)
</button>

<div style={{ marginTop: 24 }}>
<button onClick={() => dispatch(logout())} style={{ marginRight: 16 }}>
Clear Auth State (redux)
</button>

<button
onClick={() => void handleTrigger401()}
style={{ marginRight: 16 }}
disabled={isFetching}
>
Simulate 401 Error (axiosBaseQuery + redux)
</button>

<button
onClick={() => void handleRefreshCSRF()}
style={{ marginRight: 16 }}
>
Refresh CSRF Token
</button>

<button
onClick={() => void handleGetCSRF()}
style={{ marginRight: 16 }}
>
Get CSRF Token
</button>
</div>

{error && <p style={{ color: 'red' }}>401 Error Triggered</p>}

<div style={{ marginTop: 24 }}>
<h3>CSRF Token Status:</h3>
<p>Token: {csrfToken ? `${csrfToken.substring(0, 20)}...` : 'None'}</p>
<p>Valid: {isTokenValid() ? '✅ Yes' : '❌ No'}</p>
<p>Authenticated: {isAuthenticated ? '✅ Yes' : '❌ No'}</p>
</div>
</main>
);
}
6 changes: 3 additions & 3 deletions src/app/(public)/signup/component/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const ErrorMessage = styled.div`

export default function SignupForm() {
const router = useRouter();
const token = useAppSelector(s => s.auth.token);
const isAuthenticated = useAppSelector(s => s.auth.isAuthenticated);

const [signupUser, { isLoading, error }] = useSignupUserMutation();
const [showPassword, setShowPassword] = useState(false);
Expand All @@ -141,10 +141,10 @@ export default function SignupForm() {
});

useEffect(() => {
if (token) {
if (isAuthenticated) {
router.replace('/admin/overview');
}
}, [token, router]);
}, [isAuthenticated, router]);

const onSubmit = async (vals: SignupFormData) => {
const payload = {
Expand Down
11 changes: 9 additions & 2 deletions src/app/StoreProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
'use client';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';

import { store } from '@/redux/store';
import { persistor, store } from '@/redux/store';

export default function StoreProvider({
children,
}: {
children: React.ReactNode;
}) {
return <Provider store={store}>{children}</Provider>;
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
</Provider>
);
}
2 changes: 2 additions & 0 deletions src/app/admin/booking/components/TaskManager/BookingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ const BookingModal: React.FC<Props> = ({
// Reduce tolerance to 1 minute
return selectedMinutes < nowMinutes - 1;
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error in isDateTimeInPast:', error);
return false;
}
Expand Down Expand Up @@ -500,6 +501,7 @@ const BookingModal: React.FC<Props> = ({

onClose();
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to create booking:', error);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ const formatForDateTimeLocal = (isoString: string) => {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
} catch (error) {
} catch {
// If exception occurs, also use current device time
const now = new Date();
const year = now.getFullYear();
Expand Down Expand Up @@ -363,6 +363,7 @@ const EditBookingModal: React.FC<Props> = ({
// Reduce tolerance to 1 minute
return selectedMinutes < nowMinutes - 1;
} catch {
// eslint-disable-next-line no-console
console.error('Error in isDateTimeInPast');
return false;
}
Expand Down
40 changes: 33 additions & 7 deletions src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,52 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { usePathname, useRouter } from 'next/navigation';
import { type ReactNode, useEffect, useState } from 'react';

import { useCheckAuthStatusQuery } from '@/features/auth/authApi';
import {
useGetProgressQuery, // ← RTK-Query hook
} from '@/features/onboarding/onboardingApi';
import { useAppSelector } from '@/redux/hooks';

export default function ProtectedLayout({ children }: { children: ReactNode }) {
const [ready, setReady] = useState(false);
const token = useAppSelector(s => s.auth.token);
const isAuthenticated = useAppSelector(s => s.auth.isAuthenticated);
const user = useAppSelector(s => s.auth.user);
const userId = user?._id;

// Check authentication status using cookies
const { isLoading: isCheckingAuth } = useCheckAuthStatusQuery();

const {
data: progress, // { currentStep, answers, status }
isFetching,
} = useGetProgressQuery(userId ?? skipToken);
const router = useRouter();
const pathname = usePathname();

useEffect(() => {
// Wait for hydration and then check auth status
// Wait for hydration and auth check, then check auth status
const timer = setTimeout(() => {
// Don't proceed if still checking authentication
if (isCheckingAuth) {
return;
}

// check if logged in
if (!token || !user) {
if (!isAuthenticated || !user) {
router.replace('/login');
return;
}

// check if onboarding finished
if (isFetching || !progress) return;
if (isFetching) {
return;
}

// If no progress data, assume onboarding is not required or completed
if (!progress) {
setReady(true);
return;
}

if (progress.status !== 'completed' && pathname !== '/onboarding') {
router.replace('/onboarding');
Expand All @@ -43,16 +62,23 @@ export default function ProtectedLayout({ children }: { children: ReactNode }) {
}, 0);

return () => clearTimeout(timer);
}, [token, user, router, pathname, isFetching, progress]);
}, [
isAuthenticated,
user,
router,
pathname,
isFetching,
progress,
isCheckingAuth,
]);

if (!ready) {
if (!ready || isCheckingAuth) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100vh"
sx={{ visibility: 'hidden' }}
>
<Box textAlign="center">
<Box mb={2}>Initializing Admin Panel...</Box>
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/overview/components/ActivitySection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Avatar, Box, Typography, useMediaQuery } from '@mui/material';
import { styled, width } from '@mui/system';
import { styled } from '@mui/system';
import { format, isToday, parseISO } from 'date-fns';
import Image from 'next/image';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export default function CustomFormModal({
const currentField = fields.find(field => field.id === fieldId);
if (currentField && currentField.label.trim() === '') {
// label 为空时,不允许删除
// eslint-disable-next-line no-console
console.log(
'Cannot delete field with empty label. Please fill in the label first.',
);
Expand Down
Loading