Implement redirect with toast notification on profile save

- Add gotoHomeWithMessage function to navigationActions
- Redirect to home page after successful profile save
- Display success message in toast notification instead of in-form
- Check for profileSaved URL parameter on home page mount
- Clean up URL parameter after showing toast
- Move success message translation to home-page section
- Remove unused success state and message from AccountForm
- Remove useEffect import from AccountForm

User experience: After saving profile, users are redirected to the
familiar home screen and see a toast notification confirming the save.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-11-17 18:58:43 +01:00
parent 513e78e8f1
commit 5bbf80c2ae
6 changed files with 34 additions and 30 deletions

View File

@@ -9,6 +9,11 @@ export async function gotoHome({year, month}: YearMonth) {
await gotoUrl(path); await gotoUrl(path);
} }
export async function gotoHomeWithMessage(locale: string, message: string) {
const path = `/${locale}?${message}=true`;
await gotoUrl(path);
}
export async function gotoUrl(path: string) { export async function gotoUrl(path: string) {
console.log(path) console.log(path)
revalidatePath(path, "page"); revalidatePath(path, "page");

View File

@@ -7,8 +7,9 @@ import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth'; import { AuthenticatedUser } from '../types/next-auth';
import { unstable_noStore as noStore } from 'next/cache'; import { unstable_noStore as noStore } from 'next/cache';
import { IntlTemplateFn } from '@/app/i18n'; import { IntlTemplateFn } from '@/app/i18n';
import { getTranslations } from "next-intl/server"; import { getTranslations, getLocale } from "next-intl/server";
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { gotoHomeWithMessage } from './navigationActions';
export type State = { export type State = {
errors?: { errors?: {
@@ -93,8 +94,7 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt
revalidatePath('/account'); revalidatePath('/account');
return { // Get current locale and redirect to home with success message
message: null, const locale = await getLocale();
success: true, await gotoHomeWithMessage(locale, 'profileSaved');
};
}); });

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { FC, useEffect } from "react"; import { FC } from "react";
import { UserProfile } from "../lib/db-types"; import { UserProfile } from "../lib/db-types";
import { updateUserProfile } from "../lib/actions/userProfileActions"; import { updateUserProfile } from "../lib/actions/userProfileActions";
import { useFormState, useFormStatus } from "react-dom"; import { useFormState, useFormStatus } from "react-dom";
@@ -15,11 +15,10 @@ export type AccountFormProps = {
type FormFieldsProps = { type FormFieldsProps = {
profile: UserProfile | null; profile: UserProfile | null;
errors: any; errors: any;
success: boolean;
message: string | null; message: string | null;
} }
const FormFields: FC<FormFieldsProps> = ({ profile, errors, success, message }) => { const FormFields: FC<FormFieldsProps> = ({ profile, errors, message }) => {
const { pending } = useFormStatus(); const { pending } = useFormStatus();
const t = useTranslations("account-form"); const t = useTranslations("account-form");
const locale = useLocale(); const locale = useLocale();
@@ -118,16 +117,11 @@ const FormFields: FC<FormFieldsProps> = ({ profile, errors, success, message })
</div> </div>
<div id="general-error" aria-live="polite" aria-atomic="true"> <div id="general-error" aria-live="polite" aria-atomic="true">
{message && !success && ( {message && (
<p className="mt-2 text-sm text-red-500"> <p className="mt-2 text-sm text-red-500">
{message} {message}
</p> </p>
)} )}
{success && (
<p className="mt-2 text-sm text-success">
{t("success-message")}
</p>
)}
</div> </div>
<div className="pt-4"> <div className="pt-4">
@@ -147,18 +141,10 @@ const FormFields: FC<FormFieldsProps> = ({ profile, errors, success, message })
}; };
export const AccountForm: FC<AccountFormProps> = ({ profile }) => { export const AccountForm: FC<AccountFormProps> = ({ profile }) => {
const initialState = { message: null, errors: {}, success: false }; const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(updateUserProfile, initialState); const [state, dispatch] = useFormState(updateUserProfile, initialState);
const t = useTranslations("account-form"); const t = useTranslations("account-form");
useEffect(() => {
if (state.success) {
// Show success message or toast notification
// For now, we'll just log it
console.log("Profile updated successfully");
}
}, [state.success]);
return ( return (
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1"> <div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
<div className="card-body"> <div className="card-body">
@@ -167,7 +153,6 @@ export const AccountForm: FC<AccountFormProps> = ({ profile }) => {
<FormFields <FormFields
profile={profile} profile={profile}
errors={state.errors} errors={state.errors}
success={state.success ?? false}
message={state.message ?? null} message={state.message ?? null}
/> />
</form> </form>

View File

@@ -9,8 +9,9 @@ import { LocationCard } from "./LocationCard";
import { PrintButton } from "./PrintButton"; import { PrintButton } from "./PrintButton";
import { BillingLocation, YearMonth } from "../lib/db-types"; import { BillingLocation, YearMonth } from "../lib/db-types";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { ToastContainer } from 'react-toastify'; import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { useTranslations } from "next-intl";
const getNextYearMonth = (yearMonth:YearMonth) => { const getNextYearMonth = (yearMonth:YearMonth) => {
const {year, month} = yearMonth; const {year, month} = yearMonth;
@@ -38,13 +39,26 @@ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
const router = useRouter(); const router = useRouter();
const search = useSearchParams(); const search = useSearchParams();
const t = useTranslations("home-page");
const initialMonth = search.get('month') const initialMonth = search.get('month')
const [expandedMonth, setExpandedMonth] = React.useState<number>( const [expandedMonth, setExpandedMonth] = React.useState<number>(
initialMonth ? parseInt(initialMonth as string) : -1 // no month is expanded initialMonth ? parseInt(initialMonth as string) : -1 // no month is expanded
); );
// Check for profile saved success message
React.useEffect(() => {
if (search.get('profileSaved') === 'true') {
toast.success(t("profile-saved-message"), { theme: "dark" });
// Clean up URL parameter
const params = new URLSearchParams(search.toString());
params.delete('profileSaved');
const newSearch = params.toString();
router.replace(newSearch ? `?${newSearch}` : '/');
}
}, [search, router, t]);
if(!availableYears || !months) { if(!availableYears || !months) {
const currentYearMonth:YearMonth = { const currentYearMonth:YearMonth = {
year: new Date().getFullYear(), year: new Date().getFullYear(),

View File

@@ -73,7 +73,8 @@
"table-header-barcode": "2D Barcode", "table-header-barcode": "2D Barcode",
"empty-state-title": "No Barcode Data Found", "empty-state-title": "No Barcode Data Found",
"empty-state-message": "No bills with 2D barcodes found for {yearMonth}" "empty-state-message": "No bills with 2D barcodes found for {yearMonth}"
} },
"profile-saved-message": "Profile updated successfully"
}, },
"bill-delete-form": { "bill-delete-form": {
"text": "Please confirm deletion of bill \"<strong>{bill_name}</strong>\" at \"<strong>{location_name}</strong>\".", "text": "Please confirm deletion of bill \"<strong>{bill_name}</strong>\" at \"<strong>{location_name}</strong>\".",
@@ -143,7 +144,6 @@
"iban-placeholder": "Enter your IBAN", "iban-placeholder": "Enter your IBAN",
"save-button": "Save", "save-button": "Save",
"cancel-button": "Cancel", "cancel-button": "Cancel",
"success-message": "Profile updated successfully",
"validation": {} "validation": {}
} }
} }

View File

@@ -73,7 +73,8 @@
"table-header-barcode": "2D Barkod", "table-header-barcode": "2D Barkod",
"empty-state-title": "Nema Podataka o Barkodovima", "empty-state-title": "Nema Podataka o Barkodovima",
"empty-state-message": "Nema računa s 2D barkodovima za {yearMonth}" "empty-state-message": "Nema računa s 2D barkodovima za {yearMonth}"
} },
"profile-saved-message": "Profil uspješno ažuriran"
}, },
"bill-delete-form": { "bill-delete-form": {
"text": "Molim potvrdi brisanje računa \"<strong>{bill_name}</strong>\" koji pripada nekretnini \"<strong>{location_name}</strong>\".", "text": "Molim potvrdi brisanje računa \"<strong>{bill_name}</strong>\" koji pripada nekretnini \"<strong>{location_name}</strong>\".",
@@ -142,7 +143,6 @@
"iban-placeholder": "Unesite svoj IBAN", "iban-placeholder": "Unesite svoj IBAN",
"save-button": "Spremi", "save-button": "Spremi",
"cancel-button": "Odbaci", "cancel-button": "Odbaci",
"success-message": "Profil uspješno ažuriran",
"validation": {} "validation": {}
} }
} }