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 ?? '',
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 logic is incomplete. After validating that _id and email are present, the code then provides fallback values for these required fields using the nullish coalescing operator. This is inconsistent - if email is required (as checked in the validation), it shouldn't have a fallback to an empty string. Either remove the validation check for email or remove the fallback.

Suggested change
email: parsedUser.email ?? '',
email: parsedUser.email,

Copilot uses AI. Check for mistakes.
firstName: parsedUser.firstName ?? '',
lastName: parsedUser.lastName ?? '',
role: parsedUser.role ?? 'user',
status: parsedUser.status ?? 'active',
},
}),
);
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 disabled check is redundant. The condition !userInput || userInput.trim() === '' already covers all cases where the button should be disabled. The !userInput check handles both null/undefined and empty string, so checking userInput.trim() === '' afterwards is unnecessary since it will never execute when userInput is falsy. Simplify to just disabled || !userInput?.trim().

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 disabled check is redundant. The condition !userInput || userInput.trim() === '' already covers all cases where the button should be disabled. The !userInput check handles both null/undefined and empty string, so checking userInput.trim() === '' afterwards is unnecessary since it will never execute when userInput is falsy. Simplify to just disabled || !userInput?.trim().

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 ?? '');
}
}, [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 creates an infinite loop. The effect depends on inputValue, and when value changes, it updates inputValue, which triggers the effect again. This will cause continuous re-renders. Consider removing inputValue from the dependency array since you only want to sync when the external value 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);
}
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 ??
''}
Comment on lines +248 to +249
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 null check suggestion.description ?? '' is unnecessary because the earlier check !suggestion at line 243 already ensures that suggestion is not null or undefined at this point. The optional chaining on structured_formatting is sufficient.

Suggested change
suggestion.description ??
''}
suggestion.description}

Copilot uses AI. Check for mistakes.
</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