Skip to content
Closed
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
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();
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 +52 to 70
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 only verifies that _id and email exist, but then provides fallback values for other fields. However, if email is an empty string (falsy but not null/undefined), it will pass validation but be overridden to an empty string anyway. The validation should either require all critical fields or only validate fields that truly cannot have fallback values.

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() === ''}
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 condition disabled || !userInput || userInput.trim() === '' is redundant. If !userInput is true (meaning userInput is undefined, null, or empty string), then userInput.trim() === '' will either throw an error (for undefined/null) or be redundant (for empty string). The check should be simplified to disabled || !userInput?.trim() which safely handles all cases.

Copilot uses AI. Check for mistakes.
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() === ''}
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 condition disabled || !userInput || userInput.trim() === '' is redundant. If !userInput is true (meaning userInput is undefined, null, or empty string), then userInput.trim() === '' will either throw an error (for undefined/null) or be redundant (for empty string). The check should be simplified to disabled || !userInput?.trim() which safely handles all cases.

Copilot uses AI. Check for mistakes.
>
<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 ?? '');
Comment on lines +119 to +120
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 condition value !== inputValue will be true on every render when value is undefined and inputValue is '' (empty string), causing unnecessary state updates. Consider using a more precise check like value !== undefined && value !== inputValue or checking if both are empty/undefined before updating.

Suggested change
if (value !== undefined && value !== inputValue) {
setInputValue(value ?? '');
const normalizedValue = value ?? '';
if (normalizedValue !== inputValue) {
setInputValue(normalizedValue);

Copilot uses AI. Check for mistakes.
}
}, [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.

The useEffect dependency array includes inputValue which creates an infinite loop. When the effect runs and value !== inputValue, it calls setInputValue(value ?? ''), which updates inputValue. This change to inputValue triggers the effect again because inputValue is in the dependency array, creating a loop. Remove inputValue from the dependency array to fix this.

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 +179 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 state abbreviation is truncated to 3 characters using .substring(0, 3), but Australian state codes should be 2-3 characters maximum. However, the comment says "2-3 characters" but the code allows any input to be truncated to 3 chars. If the input is a full state name like "New South Wales", this would result in "NEW" instead of "NSW". Consider validating that the state is already an abbreviation or mapping full names to abbreviations.

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) {
Comment on lines +186 to +188
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 postcode validation uses .substring(0, 4) to ensure exactly 4 digits, but only adds the postcode if the length is 4. This means postcodes like "200" (3 digits after removing non-digits) would be silently dropped. Consider whether this is the intended behavior or if validation should fail more explicitly for invalid postcodes.

Suggested change
// Ensure postcode is 4 digits
const postcode = components.postalCode.replace(/\D/g, '').substring(0, 4);
if (postcode.length === 4) {
// Ensure postcode is at most 4 digits
const postcode = components.postalCode.replace(/\D/g, '').substring(0, 4);
if (postcode.length > 0) {

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