mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
Merge pull request #38 from Since-3/refactor/number-input
added number input with dynamic formatting
This commit is contained in:
commit
1c0037220c
6 changed files with 270 additions and 13 deletions
62
package-lock.json
generated
62
package-lock.json
generated
|
|
@ -36,6 +36,7 @@
|
|||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-phone-number-input": "^3.4.14",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.12",
|
||||
|
|
@ -7641,6 +7642,12 @@
|
|||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
|
|
@ -7764,6 +7771,12 @@
|
|||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
|
|
@ -9525,6 +9538,27 @@
|
|||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
|
|
@ -10023,7 +10057,6 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
|
|
@ -10186,6 +10219,12 @@
|
|||
"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": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
|
|
@ -10552,7 +10591,6 @@
|
|||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
|
|
@ -10930,7 +10968,6 @@
|
|||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -11347,7 +11384,6 @@
|
|||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
|
|
@ -11650,9 +11686,25 @@
|
|||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-phone-number-input": "^3.4.14",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.12",
|
||||
|
|
|
|||
56
src/components/layout/PhoneInputComponent.tsx
Normal file
56
src/components/layout/PhoneInputComponent.tsx
Normal 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;
|
||||
125
src/components/layout/css/phone-input.css
Normal file
125
src/components/layout/css/phone-input.css
Normal 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));
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import React, { useState } from "react";
|
||||
import AuthSidebarComponent from "../components/AuthSidebarComponent";
|
||||
import InputComponent from "@/src/components/layout/InputComponent";
|
||||
import PhoneInputComponent from "@/src/components/layout/PhoneInputComponent";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useAuthFormStore } from "../../store/authFormState";
|
||||
import { useAuth } from "@/src/hooks/useAuth";
|
||||
|
|
@ -146,13 +147,12 @@ const RegisterAdmin = () => {
|
|||
onChange={(e) => handleChange("email", e.target.value)}
|
||||
error={errors.email}
|
||||
/>
|
||||
<InputComponent
|
||||
<PhoneInputComponent
|
||||
isLabel
|
||||
label="Telefonnummer"
|
||||
type="text"
|
||||
id="register-admin-tel"
|
||||
value={formData.tel}
|
||||
onChange={(e) => handleChange("tel", e.target.value)}
|
||||
onChange={(value) => handleChange("tel", value || "")}
|
||||
error={errors.tel}
|
||||
/>
|
||||
<InputComponent
|
||||
|
|
@ -265,13 +265,12 @@ const RegisterAdmin = () => {
|
|||
onChange={(e) => handleChange("companyMail", e.target.value)}
|
||||
error={errors.companyMail}
|
||||
/>
|
||||
<InputComponent
|
||||
<PhoneInputComponent
|
||||
isLabel
|
||||
label="Geschäftsnummer (falls unterschiedlich)"
|
||||
type="text"
|
||||
id="register-admin-companyNumber"
|
||||
value={formData.companyNumber}
|
||||
onChange={(e) => handleChange("companyNumber", e.target.value)}
|
||||
onChange={(value) => handleChange("companyNumber", value || "")}
|
||||
error={errors.companyNumber}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
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
|
||||
export const adminRegisterStep1Schema = z
|
||||
.object({
|
||||
fullName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"),
|
||||
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"),
|
||||
passwordConfirm: z.string(),
|
||||
})
|
||||
|
|
@ -35,7 +47,19 @@ export const adminRegisterStep3Schema = z.object({
|
|||
.email("Bitte eine gültige E-Mail-Adresse eingeben")
|
||||
.optional()
|
||||
.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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue