add: login page fundamentals

This commit is contained in:
Mahyar Khanbolooki
2025-06-30 21:17:35 +03:30
parent 268782fcc5
commit 09d1093253
28 changed files with 563 additions and 22 deletions
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
type Props = { } & React.ButtonHTMLAttributes<HTMLButtonElement>;
function PrimaryButton({children, ...rest}: Props) {
return (
<button className='bg-primary w-full rounded-normal p-3 text-white font-bold text-sm' {...rest}>
{ children }
</button>
)
}
export default PrimaryButton
+70
View File
@@ -0,0 +1,70 @@
import { motion } from "framer-motion";
import React from "react";
type EyeToggleIconProps = {
slash: boolean;
} & React.SVGProps<SVGSVGElement>;
export const EyeToggleIcon: React.FC<EyeToggleIconProps> = ({ slash, ...props }) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<defs>
<filter id="whiteShadow" x="-50%" y="-50%" width="200%" height="200%" colorInterpolationFilters="sRGB" >
<feDropShadow dx="1.5" dy="0" stdDeviation="0" floodColor="white" floodOpacity="1" />
</filter>
</defs>
{/* Eye Circle */}
<path
d="M12.9833 9.99994C12.9833 11.6499 11.6499 12.9833 9.99994 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99994C7.0166 8.34993 8.34993 7.0166 9.99994 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99994Z"
stroke="#333"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Eye Outline */}
<path
d="M10.0001 16.8918C12.9418 16.8918 15.6834 15.1584 17.5918 12.1584C18.3418 10.9834 18.3418 9.00843 17.5918 7.83343C15.6834 4.83343 12.9418 3.1001 10.0001 3.1001C7.05845 3.1001 4.31678 4.83343 2.40845 7.83343C1.65845 9.00843 1.65845 10.9834 2.40845 12.1584C4.31678 15.1584 7.05845 16.8918 10.0001 16.8918Z"
stroke="#333"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Slash Line - Animate with Framer Motion */}
<motion.path
d="M19.6 1.66675L1.66699 19.6"
stroke="#FFF"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
initial={false}
animate={{ strokeWidth: slash ? 0 : 1.2, opacity: slash ? 0 : 1 }}
transition={{
strokeWidth: { duration: 0.2, ease: "easeInOut" },
opacity: { duration: 0.1, ease: "easeInOut", delay: slash ? 0.1 : 0 }
}}
/>
<motion.path
d="M18.3332 1.66675L1.66699 18.3334"
stroke="#333"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
initial={false}
animate={{ pathLength: slash ? 0 : 1, opacity: slash ? 0 : 1 }}
transition={{
pathLength: { duration: 0.3, ease: "easeInOut" },
opacity: { duration: 0.2, ease: "easeInOut", delay: slash ? 0.1 : 0 }
}}
/>
</svg>
);
};
+29
View File
@@ -0,0 +1,29 @@
import React from 'react'
type Props = {
htmlFor: string;
labelText?: React.ReactNode;
value?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "id">;
function InputField({ onChange, htmlFor, labelText, children, className, ...inputProps }: Props) {
return (
<div className={`${className} h-11 inline-flex relative border border-[#E5E5E5] px-3 w-full rounded-normal group focus-within:border focus-within:border-primary`}>
<label
className='absolute right-2 -top-3 px-2 bg-background text-[12px] text-foreground'
htmlFor={htmlFor}>
{labelText}
</label>
<input
onChange={onChange}
className='py-2.5 text-base w-full outline-0 !leading-6'
id={htmlFor}
{...inputProps} />
{children}
</div>
)
}
export default InputField
+77
View File
@@ -0,0 +1,77 @@
'use client';
import React, { createRef, useEffect, useRef, useState } from 'react'
type Props = {
htmlFor: string;
labelText?: React.ReactNode;
value?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
maxLength?: number;
className?: string;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "id">;
function OTPInputField({ onChange, maxLength = 6, htmlFor, labelText, className, value = '' }: Props) {
const values = value?.toString().split('').slice(0, maxLength);
const inputRefs = useRef(Array.from({ length: maxLength }, () => createRef<HTMLInputElement>()));
const [prevLength, setPrevLength] = useState(0);
useEffect(() => {
const valLength = values.length;
const focusIndex = valLength < prevLength
? Math.max(0, valLength)
: Math.min(valLength, maxLength - 1);
inputRefs.current[focusIndex]?.current?.focus();
setPrevLength(valLength);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const keyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const val = String(value);
const target = e.target as HTMLInputElement;
const index = +target.id.split('-')[2];
if (e.key == "Backspace") {
let i = Math.max(0, val.length - 1)
if (val.length >= maxLength && index < maxLength) {
i = maxLength - 1;
}
inputRefs.current[i]?.current?.focus();
setPrevLength(() => i)
} else {
const i = Math.max(0, val.length)
inputRefs.current[i]?.current?.focus();
setPrevLength(() => i)
}
}
return (
<div className={`${className} h-14 inline-flex relative border-none w-full`}>
<label
className='absolute right-0 -top-6 px-2 bg-background text-[12px] text-foreground'
htmlFor={htmlFor}>
{labelText}
</label>
<div className='inline-flex justify-between items-center gap-2'>
{inputRefs?.current?.map((ref, i) => {
return (
<input
key={i}
onKeyDown={keyDown}
onChange={onChange}
id={`${htmlFor}-data-${i}`}
readOnly={Math.max(0, values?.length) != i && prevLength != i}
ref={ref}
value={values?.at(i) ?? ''}
maxLength={1}
className='outline-none text-center inline-flex items-center justify-center h-full relative border border-[#E5E5E5] w-full rounded-normal group focus-within:border focus-within:border-primary' />
)
})}
</div>
</div>
)
}
export default OTPInputField