Added informational alert at the top of the account form to explain that profile data (name, address, IBAN) will be used to generate 2D barcodes for tenant bill payments. The alert uses a horizontal layout on all screen sizes for consistent UX. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
186 lines
7.3 KiB
TypeScript
186 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import { FC } from "react";
|
|
import { UserProfile } from "../lib/db-types";
|
|
import { updateUserProfile } from "../lib/actions/userProfileActions";
|
|
import { useFormState, useFormStatus } from "react-dom";
|
|
import { useLocale, useTranslations } from "next-intl";
|
|
import Link from "next/link";
|
|
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
|
|
|
export type AccountFormProps = {
|
|
profile: UserProfile | null;
|
|
}
|
|
|
|
type FormFieldsProps = {
|
|
profile: UserProfile | null;
|
|
errors: any;
|
|
message: string | null;
|
|
}
|
|
|
|
const FormFields: FC<FormFieldsProps> = ({ profile, errors, message }) => {
|
|
const { pending } = useFormStatus();
|
|
const t = useTranslations("account-form");
|
|
const locale = useLocale();
|
|
|
|
return (
|
|
<>
|
|
<div className="alert max-w-md flex flex-row items-start">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-6 h-6">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<span className="text-left">{t("info-box-message")}</span>
|
|
</div>
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text">{t("first-name-label")}</span>
|
|
</label>
|
|
<input
|
|
id="firstName"
|
|
name="firstName"
|
|
type="text"
|
|
placeholder={t("first-name-placeholder")}
|
|
className="input input-bordered w-full"
|
|
defaultValue={profile?.firstName ?? ""}
|
|
disabled={pending}
|
|
/>
|
|
<div id="firstName-error" aria-live="polite" aria-atomic="true">
|
|
{errors?.firstName &&
|
|
errors.firstName.map((error: string) => (
|
|
<p className="mt-2 text-sm text-red-500" key={error}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text">{t("last-name-label")}</span>
|
|
</label>
|
|
<input
|
|
id="lastName"
|
|
name="lastName"
|
|
type="text"
|
|
placeholder={t("last-name-placeholder")}
|
|
className="input input-bordered w-full"
|
|
defaultValue={profile?.lastName ?? ""}
|
|
disabled={pending}
|
|
/>
|
|
<div id="lastName-error" aria-live="polite" aria-atomic="true">
|
|
{errors?.lastName &&
|
|
errors.lastName.map((error: string) => (
|
|
<p className="mt-2 text-sm text-red-500" key={error}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text">{t("address-label")}</span>
|
|
</label>
|
|
<textarea
|
|
id="address"
|
|
name="address"
|
|
className="textarea textarea-bordered w-full"
|
|
placeholder={t("address-placeholder")}
|
|
defaultValue={profile?.address ?? ""}
|
|
disabled={pending}
|
|
></textarea>
|
|
<div id="address-error" aria-live="polite" aria-atomic="true">
|
|
{errors?.address &&
|
|
errors.address.map((error: string) => (
|
|
<p className="mt-2 text-sm text-red-500" key={error}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text">{t("iban-label")}</span>
|
|
</label>
|
|
<input
|
|
id="iban"
|
|
name="iban"
|
|
type="text"
|
|
placeholder={t("iban-placeholder")}
|
|
className="input input-bordered w-full"
|
|
defaultValue={profile?.iban ?? ""}
|
|
disabled={pending}
|
|
/>
|
|
<div id="iban-error" aria-live="polite" aria-atomic="true">
|
|
{errors?.iban &&
|
|
errors.iban.map((error: string) => (
|
|
<p className="mt-2 text-sm text-red-500" key={error}>
|
|
{error}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div id="general-error" aria-live="polite" aria-atomic="true">
|
|
{message && (
|
|
<p className="mt-2 text-sm text-red-500">
|
|
{message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="pt-4">
|
|
<button className="btn btn-primary w-[5.5em]" disabled={pending}>
|
|
{pending ? (
|
|
<span className="loading loading-spinner loading-sm"></span>
|
|
) : (
|
|
t("save-button")
|
|
)}
|
|
</button>
|
|
<Link className={`btn btn-neutral w-[5.5em] ml-3 ${pending ? "btn-disabled" : ""}`} href={`/${locale}`}>
|
|
{t("cancel-button")}
|
|
</Link>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const AccountForm: FC<AccountFormProps> = ({ profile }) => {
|
|
const initialState = { message: null, errors: {} };
|
|
const [state, dispatch] = useFormState(updateUserProfile, initialState);
|
|
const t = useTranslations("account-form");
|
|
|
|
return (
|
|
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
|
|
<div className="card-body">
|
|
<h2 className="card-title"><AccountCircleIcon className="w-6 h-6" /> {t("title")}</h2>
|
|
<form action={dispatch}>
|
|
<FormFields
|
|
profile={profile}
|
|
errors={state.errors}
|
|
message={state.message ?? null}
|
|
/>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const AccountFormSkeleton: FC = () => {
|
|
return (
|
|
<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="h-8 w-32 skeleton mb-4"></div>
|
|
<div className="input w-full skeleton"></div>
|
|
<div className="input w-full skeleton mt-4"></div>
|
|
<div className="textarea w-full h-24 skeleton mt-4"></div>
|
|
<div className="input w-full skeleton mt-4"></div>
|
|
<div className="pt-4">
|
|
<div className="btn skeleton w-24"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|