Skip to content
Closed
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint --fix",
Expand Down
4 changes: 2 additions & 2 deletions src/app/(public)/blogs/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// src/app/blogs/[id]/page.tsx
import type { Blog } from '@/types/blog';
import { getApiBaseUrl } from '@/utils/api-config';

import IntroSection from './components/IntroSection';

Expand All @@ -10,8 +11,7 @@ export default async function DetailBlogPage({
}) {
// params.id is a plain string here
const { id } = await params;
const baseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:4000/api';
const baseUrl = getApiBaseUrl();
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This server component is calling getApiBaseUrl() which relies on process.env.NODE_ENV at runtime. In Next.js server components, this will always be 'production' during the build process, even in development mode. This means the localhost fallback in getApiBaseUrl() won't work, and the function will throw an error if NEXT_PUBLIC_API_BASE_URL is not set during development builds. For server components, you should directly access process.env.NEXT_PUBLIC_API_BASE_URL or use a different approach that doesn't rely on NODE_ENV.

Copilot uses AI. Check for mistakes.
const res = await fetch(`${baseUrl}/blogs/${id}`, {
cache: 'no-store',
});
Expand Down
7 changes: 3 additions & 4 deletions src/app/(public)/blogs/components/BlogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react';

import theme from '@/theme';
import type { Blog } from '@/types/blog';
import { getApiBaseUrl } from '@/utils/api-config';

import BlogCard from './BlogCard';

Expand Down Expand Up @@ -51,8 +52,7 @@ export default function BlogList() {
if (keyword.trim()) params.set('keyword', keyword.trim());
if (topic.trim()) params.set('topic', topic.trim());

const baseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:4000/api';
const baseUrl = getApiBaseUrl();
const url = `${baseUrl}/blogs/search?${params.toString()}`;

const res = await axios.get<{
Expand Down Expand Up @@ -82,8 +82,7 @@ export default function BlogList() {
if (keyword.trim()) nextParams.set('keyword', keyword.trim());
if (topic.trim()) nextParams.set('topic', topic.trim());

const baseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:4000/api';
const baseUrl = getApiBaseUrl();
const checkUrl = `${baseUrl}/blogs/search?${nextParams.toString()}`;

const res = await axios.get<{ data: Blog[] }>(checkUrl);
Expand Down
4 changes: 2 additions & 2 deletions src/app/(public)/blogs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Suspense, useEffect, useState } from 'react';

import theme from '@/theme';
import type { Blog } from '@/types/blog';
import { getApiBaseUrl } from '@/utils/api-config';

import Banner from './components/Banner';
import BlogFilterBar from './components/BlogFilterBar';
Expand All @@ -21,8 +22,7 @@ export default function BlogsPage() {

const fetchHighlightBlogs = async () => {
try {
const baseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:4000/api';
const baseUrl = getApiBaseUrl();

const res = await axios.get<Blog[]>(`${baseUrl}/blogs/highlights`, {
params: { limit: 3 },
Expand Down
18 changes: 12 additions & 6 deletions src/app/auth/callback/AuthCallbackContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ export default function AuthCallbackContent() {
// Clear any persisted auth state to prevent old user ID from being used
localStorage.removeItem('persist:root');

console.log('[AuthCallback] Setting user with ID:', parsedUser._id);
// Validate parsed user data
if (!parsedUser._id || !parsedUser.email) {
// eslint-disable-next-line no-console
console.error('[AuthCallback] Invalid user data:', parsedUser);
router.replace('/login?error=oauth_invalid_data');
return;
}

dispatch(
setCredentials({
csrfToken,
user: {
_id: parsedUser._id,
email: parsedUser.email,
firstName: parsedUser.firstName,
lastName: parsedUser.lastName,
role: parsedUser.role,
status: parsedUser.status,
email: parsedUser.email ?? '',
firstName: parsedUser.firstName ?? '',
lastName: parsedUser.lastName ?? '',
role: parsedUser.role ?? 'user',
status: parsedUser.status ?? 'active',
Comment on lines +65 to +69
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation check on line 53 ensures that _id and email exist before using them. However, lines 65-69 still use the nullish coalescing operator (??) for these fields even though they're guaranteed to be defined at this point. This is redundant and could mask issues. Since the validation already ensures these fields exist, you can use them directly without the fallback operators for _id and email.

Copilot uses AI. Check for mistakes.
},
}),
);
Expand Down
18 changes: 9 additions & 9 deletions src/app/onboarding/components/UserInputArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface AddressComponents {
}

interface UserInputAreaProps {
userInput: string;
userInput: string | undefined;
setUserInput: (value: string) => void;
onTextSubmit: (input: string) => void;
disabled?: boolean;
Expand Down Expand Up @@ -73,7 +73,7 @@ export default function UserInputArea({
onAddressSelect,
}: UserInputAreaProps) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && userInput.trim()) {
if (e.key === 'Enter' && userInput?.trim()) {
e.preventDefault();
onTextSubmit(userInput);
}
Expand All @@ -91,7 +91,7 @@ export default function UserInputArea({
};

const handleAddressKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && userInput.trim()) {
if (e.key === 'Enter' && userInput?.trim()) {
e.preventDefault();
onTextSubmit(userInput);
}
Expand All @@ -103,7 +103,7 @@ export default function UserInputArea({
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1 }}>
<Box sx={{ flex: 1 }}>
<AddressAutocomplete
value={userInput}
value={userInput ?? ''}
onChange={setUserInput}
onAddressSelect={handleAddressSelect}
displayFullAddress={true}
Expand All @@ -113,8 +113,8 @@ export default function UserInputArea({
/>
</Box>
<SendIconBtn
onClick={() => onTextSubmit(userInput)}
disabled={disabled || userInput.trim() === ''}
onClick={() => onTextSubmit(userInput ?? '')}
disabled={disabled || !userInput || userInput.trim() === ''}
sx={{ mb: 0.5 }}
>
<ArrowUpwardRoundedIcon fontSize="small" />
Expand All @@ -128,16 +128,16 @@ export default function UserInputArea({
fullWidth
variant="outlined"
placeholder="Enter your message..."
value={userInput}
value={userInput ?? ''}
onChange={e => setUserInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={disabled}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SendIconBtn
onClick={() => onTextSubmit(userInput)}
disabled={disabled || userInput.trim() === ''}
onClick={() => onTextSubmit(userInput ?? '')}
disabled={disabled || !userInput || userInput.trim() === ''}
>
<ArrowUpwardRoundedIcon fontSize="small" />
</SendIconBtn>
Expand Down
5 changes: 3 additions & 2 deletions src/components/GoogleOAuthButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Image from 'next/image';
import { useCallback } from 'react';
import styled from 'styled-components';

import { getApiBaseUrl } from '@/utils/api-config';

const GoogleButton = styled.button`
display: flex;
align-items: center;
Expand Down Expand Up @@ -90,8 +92,7 @@ export default function GoogleOAuthButton({
text = 'Sign in with Google',
}: GoogleOAuthButtonProps) {
const handleGoogleLogin = useCallback(() => {
const backendUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:4000/api';
const backendUrl = getApiBaseUrl();
window.location.href = `${backendUrl}/auth/google`;
}, []);

Expand Down
91 changes: 66 additions & 25 deletions src/components/ui/AddressAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface AddressComponents {
}

interface AddressAutocompleteProps {
value: string;
value: string | undefined;
onChange: (value: string) => void;
onAddressSelect: (
address: string,
Expand Down Expand Up @@ -87,19 +87,20 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
}) => {
const [suggestions, setSuggestions] = useState<AutocompletePrediction[]>([]);
const [loading, setLoading] = useState(false);
const [inputValue, setInputValue] = useState(value);
const [inputValue, setInputValue] = useState(value ?? '');

// Debounced fetch for autocomplete
const debouncedFetch = useMemo(
() =>
debounce(async (input: string) => {
if (!input || input.trim().length < 2) {
debounce(async (input: string | undefined) => {
const inputStr = input ?? '';
if (!inputStr || inputStr.trim().length < 2) {
setSuggestions([]);
return;
}
setLoading(true);
try {
const data = await fetchPlaceAutocomplete(input, {
const data = await fetchPlaceAutocomplete(inputStr, {
country: 'au',
types: 'address',
});
Expand All @@ -113,8 +114,15 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
[],
);

// Sync inputValue with value prop when it changes externally
useEffect(() => {
void debouncedFetch(inputValue);
if (value !== undefined && value !== inputValue) {
setInputValue(value ?? '');
}
}, [value, inputValue]);
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect has a dependency on 'inputValue' which is also being updated inside the effect (via setInputValue). This creates an infinite loop: when 'value' changes, inputValue is set, which triggers this effect again, which sets inputValue again, and so on. The dependency array should only include 'value', not 'inputValue', since you only want to sync when the external prop changes.

Suggested change
}, [value, inputValue]);
}, [value]);

Copilot uses AI. Check for mistakes.

useEffect(() => {
void debouncedFetch(inputValue ?? '');
return () => {
debouncedFetch.cancel();
};
Expand Down Expand Up @@ -148,22 +156,44 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
return parsed;
};

// Format address for display
// Format address for display - ensure it matches backend regex pattern
// Backend expects: "Street, Suburb, State Postcode" (e.g., "123 Main St, Sydney, NSW 2000")
const formatStructuredAddress = (components: AddressComponents): string => {
const parts = [];

// Build street address
if (components.streetNumber && components.route) {
parts.push(`${components.streetNumber} ${components.route}`);
} else if (components.route) {
parts.push(components.route);
}

// Add suburb
if (components.locality) {
parts.push(components.locality);
}

// Combine state and postcode (backend expects "State Postcode" format)
const statePostcode = [];
if (components.administrativeAreaLevel1)
statePostcode.push(components.administrativeAreaLevel1);
if (components.postalCode) statePostcode.push(components.postalCode);
if (statePostcode.length > 0) parts.push(statePostcode.join(' '));
if (components.administrativeAreaLevel1) {
// Ensure state is uppercase and 2-3 characters
const state = components.administrativeAreaLevel1
.toUpperCase()
.substring(0, 3);
statePostcode.push(state);
Comment on lines +180 to +183
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Ensure state is uppercase and 2-3 characters", but the code uses substring(0, 3) which always limits to a maximum of 3 characters. This doesn't enforce a minimum of 2 characters. If the state abbreviation is less than 2 characters, it should be validated or handled appropriately. Consider adding validation to ensure the state code meets the expected format (e.g., 2-3 uppercase letters).

Suggested change
const state = components.administrativeAreaLevel1
.toUpperCase()
.substring(0, 3);
statePostcode.push(state);
const rawState = components.administrativeAreaLevel1.toUpperCase();
if (rawState.length >= 2) {
const state = rawState.substring(0, 3);
statePostcode.push(state);
}

Copilot uses AI. Check for mistakes.
}
if (components.postalCode) {
// Ensure postcode is 4 digits
const postcode = components.postalCode.replace(/\D/g, '').substring(0, 4);
if (postcode.length === 4) {
statePostcode.push(postcode);
}
}

if (statePostcode.length > 0) {
parts.push(statePostcode.join(' '));
}

return parts.join(', ');
};

Expand Down Expand Up @@ -207,27 +237,34 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
}
};

const formatAddressForDisplay = (suggestion: AutocompletePrediction) => (
<SuggestionItem>
<MainText variant="body1">
{suggestion.structured_formatting?.main_text ?? suggestion.description}
</MainText>
<SecondaryText variant="body2">
{suggestion.structured_formatting?.secondary_text ?? ''}
</SecondaryText>
</SuggestionItem>
);
const formatAddressForDisplay = (
suggestion: AutocompletePrediction | null | undefined,
) => {
if (!suggestion) return null;
return (
<SuggestionItem>
<MainText variant="body1">
{suggestion.structured_formatting?.main_text ??
suggestion.description ??
''}
</MainText>
<SecondaryText variant="body2">
{suggestion.structured_formatting?.secondary_text ?? ''}
</SecondaryText>
</SuggestionItem>
);
};

return (
<Box>
<StyledAutocomplete
options={suggestions}
options={suggestions || []}
getOptionLabel={option =>
typeof option === 'string'
? option
: (option as AutocompletePrediction).description
: (option as AutocompletePrediction)?.description || ''
}
inputValue={inputValue}
inputValue={inputValue ?? ''}
onInputChange={handleInputChange}
onChange={(event, value) => {
void handleOptionSelect(
Expand Down Expand Up @@ -259,9 +296,13 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
renderOption={(props, option) => {
const { key, ...otherProps } =
props as React.HTMLAttributes<HTMLLIElement> & { key: React.Key };
const display = formatAddressForDisplay(
option as AutocompletePrediction | null | undefined,
);
if (!display) return null;
return (
<li key={key} {...otherProps}>
{formatAddressForDisplay(option as AutocompletePrediction)}
{display}
</li>
);
}}
Expand Down
3 changes: 2 additions & 1 deletion src/features/public/publicApiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

import type { Plan } from '@/types/plan.types';
import { getApiBaseUrl } from '@/utils/api-config';

interface HealthResponse {
status: string;
Expand All @@ -10,7 +11,7 @@ interface HealthResponse {
export const publicApiSlice = createApi({
reducerPath: 'publicApi',
baseQuery: fetchBaseQuery({
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
baseUrl: getApiBaseUrl(),
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling getApiBaseUrl() at module initialization can cause issues if this module is imported during server-side rendering or build time. The baseUrl for fetchBaseQuery is evaluated when the module loads, not when the query runs. This means the NODE_ENV check in getApiBaseUrl() may execute in an unexpected context. Consider using a dynamic baseQuery that calls getApiBaseUrl() at query time, or ensure this slice is only used in client-side contexts.

Copilot uses AI. Check for mistakes.
responseHandler: 'content-type',
}),
endpoints: builder => ({
Expand Down
Loading