added number input with dynamic formatting

This commit is contained in:
Rajpreet0 2026-01-23 18:13:24 +01:00
parent a050a23cbe
commit cdc16a0663
6 changed files with 270 additions and 13 deletions

62
package-lock.json generated
View file

@ -36,6 +36,7 @@
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-phone-number-input": "^3.4.14",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"zod": "^4.1.12", "zod": "^4.1.12",
@ -7641,6 +7642,12 @@
"url": "https://polar.sh/cva" "url": "https://polar.sh/cva"
} }
}, },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@ -7764,6 +7771,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/country-flag-icons": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.8.tgz",
"integrity": "sha512-VpRAbqZ+uUuC9OHgIZwqeDnWG4YwkD8s6jBZXlTK4WBYj3QXeS+/uCi4u2eAUdZ5eKjIAlkU5UYV/Z4RPvhYJg==",
"license": "MIT"
},
"node_modules/crelt": { "node_modules/crelt": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@ -9525,6 +9538,27 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/input-format": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.14.tgz",
"integrity": "sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">=18.1.0",
"react-dom": ">=18.1.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -10023,7 +10057,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@ -10186,6 +10219,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/libphonenumber-js": {
"version": "1.12.35",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.35.tgz",
"integrity": "sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==",
"license": "MIT"
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.2", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@ -10552,7 +10591,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@ -10930,7 +10968,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -11347,7 +11384,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@ -11650,9 +11686,25 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-phone-number-input": {
"version": "3.4.14",
"resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.14.tgz",
"integrity": "sha512-T9MziNuvthzv6+JAhKD71ab/jVXW5U20nQZRBJd6+q+ujmkC+/ISOf2GYo8pIi4VGjdIYRIHDftMAYn3WKZT3w==",
"license": "MIT",
"dependencies": {
"classnames": "^2.5.1",
"country-flag-icons": "^1.5.17",
"input-format": "^0.3.14",
"libphonenumber-js": "^1.12.27",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",

View file

@ -41,6 +41,7 @@
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-phone-number-input": "^3.4.14",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"zod": "^4.1.12", "zod": "^4.1.12",

View file

@ -0,0 +1,56 @@
"use client";
import { Label } from "../ui/label";
import PhoneInput, { type Value } from "react-phone-number-input";
import "react-phone-number-input/style.css";
import "./css/phone-input.css";
interface PhoneInputProps {
label?: string;
id: string;
isLabel?: boolean;
placeholder?: string;
onChange?: (value: string) => void;
value?: string;
error?: string;
}
const PhoneInputComponent: React.FC<PhoneInputProps> = ({
label,
id,
placeholder,
isLabel,
onChange,
value,
error,
}) => {
const handleChange = (newValue: Value) => {
if (onChange) {
onChange(newValue || "");
}
};
return (
<div>
{isLabel && (
<Label htmlFor={id} className="p-1 mb-2 text-blue text-lg font-semibold">
{label}
</Label>
)}
<div className="relative">
<PhoneInput
international
defaultCountry="DE"
value={value as Value}
onChange={handleChange}
placeholder={placeholder || "Telefonnummer eingeben"}
id={id}
className="phone-input-custom"
/>
</div>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
</div>
);
};
export default PhoneInputComponent;

View file

@ -0,0 +1,125 @@
.phone-input-custom {
width: 100%;
position: relative;
}
/* Main input field - matching InputComponent styling */
.phone-input-custom .PhoneInputInput {
height: 50px;
border: 1px solid;
border-color: #1e293b;
border-radius: 0.75rem;
width: 100%;
padding: 0.25rem 1rem 0.25rem 4.5rem;
font-size: 1.125rem; /* text-lg */
line-height: 1.75rem;
outline: none;
transition: all 0.2s;
background-color: transparent;
color: hsl(var(--foreground));
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
@media (max-width: 768px) {
.phone-input-custom .PhoneInputInput {
font-size: 1rem; /* text-base on mobile */
}
}
/* Country selector container */
.phone-input-custom .PhoneInputCountry {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
z-index: 10;
display: flex;
align-items: center;
gap: 0.25rem;
pointer-events: none;
}
/* Flag icon */
.phone-input-custom .PhoneInputCountryIcon {
width: 1.5rem;
height: 1.125rem;
border-radius: 0.125rem;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.phone-input-custom .PhoneInputCountryIcon--border {
border: 1px solid rgba(0, 0, 0, 0.1);
}
.phone-input-custom .PhoneInputCountryIcon img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Country select dropdown (invisible overlay) */
.phone-input-custom .PhoneInputCountrySelect {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 4rem;
opacity: 0;
cursor: pointer;
z-index: 11;
pointer-events: auto;
}
/* Dropdown arrow */
.phone-input-custom .PhoneInputCountrySelectArrow {
width: 0.5rem;
height: 0.5rem;
color: hsl(var(--blue));
opacity: 0.6;
margin-left: 0.25rem;
}
/* Focus state */
.phone-input-custom .PhoneInputInput:focus {
outline: none;
border-color: hsl(var(--blue));
box-shadow: 0 0 0 2px hsl(var(--blue) / 0.2);
}
/* Placeholder */
.phone-input-custom .PhoneInputInput::placeholder {
color: hsl(var(--muted-foreground));
}
/* Disabled state */
.phone-input-custom .PhoneInputInput:disabled {
pointer-events: none;
cursor: not-allowed;
opacity: 0.5;
}
/* Dark mode support */
.dark .phone-input-custom .PhoneInputInput {
background-color: hsl(var(--input) / 0.3);
color: hsl(var(--foreground));
}
.dark .phone-input-custom .PhoneInputCountryIcon--border {
border-color: rgba(255, 255, 255, 0.1);
}
/* Error state */
.phone-input-custom.error .PhoneInputInput {
border-color: hsl(var(--destructive));
}
.phone-input-custom.error .PhoneInputInput:focus {
box-shadow: 0 0 0 2px hsl(var(--destructive) / 0.2);
}
/* Selection styling */
.phone-input-custom .PhoneInputInput::selection {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}

View file

@ -2,6 +2,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import AuthSidebarComponent from "../components/AuthSidebarComponent"; import AuthSidebarComponent from "../components/AuthSidebarComponent";
import InputComponent from "@/src/components/layout/InputComponent"; import InputComponent from "@/src/components/layout/InputComponent";
import PhoneInputComponent from "@/src/components/layout/PhoneInputComponent";
import { Button } from "@/src/components/ui/button"; import { Button } from "@/src/components/ui/button";
import { useAuthFormStore } from "../../store/authFormState"; import { useAuthFormStore } from "../../store/authFormState";
import { useAuth } from "@/src/hooks/useAuth"; import { useAuth } from "@/src/hooks/useAuth";
@ -146,13 +147,12 @@ const RegisterAdmin = () => {
onChange={(e) => handleChange("email", e.target.value)} onChange={(e) => handleChange("email", e.target.value)}
error={errors.email} error={errors.email}
/> />
<InputComponent <PhoneInputComponent
isLabel isLabel
label="Telefonnummer" label="Telefonnummer"
type="text"
id="register-admin-tel" id="register-admin-tel"
value={formData.tel} value={formData.tel}
onChange={(e) => handleChange("tel", e.target.value)} onChange={(value) => handleChange("tel", value || "")}
error={errors.tel} error={errors.tel}
/> />
<InputComponent <InputComponent
@ -265,13 +265,12 @@ const RegisterAdmin = () => {
onChange={(e) => handleChange("companyMail", e.target.value)} onChange={(e) => handleChange("companyMail", e.target.value)}
error={errors.companyMail} error={errors.companyMail}
/> />
<InputComponent <PhoneInputComponent
isLabel isLabel
label="Geschäftsnummer (falls unterschiedlich)" label="Geschäftsnummer (falls unterschiedlich)"
type="text"
id="register-admin-companyNumber" id="register-admin-companyNumber"
value={formData.companyNumber} value={formData.companyNumber}
onChange={(e) => handleChange("companyNumber", e.target.value)} onChange={(value) => handleChange("companyNumber", value || "")}
error={errors.companyNumber} error={errors.companyNumber}
/> />

View file

@ -1,11 +1,23 @@
import { z } from "zod"; import { z } from "zod";
// Helper function to validate international phone numbers
const isValidPhoneNumber = (phone: string): boolean => {
// Check if phone starts with + and has at least country code and number
const phoneRegex = /^\+[1-9]\d{1,14}$/;
return phoneRegex.test(phone);
};
// Step 1: Personal Info // Step 1: Personal Info
export const adminRegisterStep1Schema = z export const adminRegisterStep1Schema = z
.object({ .object({
fullName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"), fullName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"),
email: z.string().email("Bitte eine gültige E-Mail-Adresse eingeben"), email: z.string().email("Bitte eine gültige E-Mail-Adresse eingeben"),
tel: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein"), tel: z
.string()
.min(1, "Telefonnummer ist erforderlich")
.refine((val) => isValidPhoneNumber(val), {
message: "Bitte eine gültige Telefonnummer eingeben",
}),
password: z.string().min(8, "Passwort muss mindestens 8 Zeichen lang sein"), password: z.string().min(8, "Passwort muss mindestens 8 Zeichen lang sein"),
passwordConfirm: z.string(), passwordConfirm: z.string(),
}) })
@ -35,7 +47,19 @@ export const adminRegisterStep3Schema = z.object({
.email("Bitte eine gültige E-Mail-Adresse eingeben") .email("Bitte eine gültige E-Mail-Adresse eingeben")
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
companyNumber: z.string().optional().or(z.literal("")), companyNumber: z
.string()
.optional()
.or(z.literal(""))
.refine(
(val) => {
if (!val || val === "") return true;
return isValidPhoneNumber(val);
},
{
message: "Bitte eine gültige Telefonnummer eingeben",
}
),
}); });
// Full schema for complete validation // Full schema for complete validation