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
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',
},
}),
);
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]);

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 ??
''}
</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