diff --git a/package-lock.json b/package-lock.json index 3e0a720..c48d263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "dmenu-plus-frontned", "version": "0.1.0", "dependencies": { + "dmenu-plus-frontned": "file:", + "framer-motion": "^12.20.1", "next": "15.3.4", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -2527,6 +2529,10 @@ "node": ">=8" } }, + "node_modules/dmenu-plus-frontned": { + "resolved": "", + "link": true + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3333,6 +3339,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.20.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.20.1.tgz", + "integrity": "sha512-NW2t2GHQcNvLHq18JyNVY15VKrwru+nkNyhLdqf4MbxbGhxZcSDi68iNcAy6O1nG0yYAQJbLioBIH1Kmg8Xr1g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.20.1", + "motion-utils": "^12.19.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4605,6 +4638,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.20.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.20.1.tgz", + "integrity": "sha512-XyveLJ9dmQTmaEsP9RlcuoNFxWlRIGdasdPJBB4aOwPr8bRcJdhltudAbiEjRQBmsGD30sjJdaEjhkHsAHapLQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.19.0" + } + }, + "node_modules/motion-utils": { + "version": "12.19.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", + "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index f5c4c5f..ffa8134 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,21 @@ "lint": "next lint" }, "dependencies": { + "dmenu-plus-frontned": "file:", + "framer-motion": "^12.20.1", + "next": "15.3.4", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.4" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.4", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/public/assets/css/fonts.css b/public/assets/css/fonts.css new file mode 100644 index 0000000..ba5122a --- /dev/null +++ b/public/assets/css/fonts.css @@ -0,0 +1,43 @@ +/* Irancell Font Section */ +@font-face { + font-family: 'Irancell'; + src: url('/assets/fonts/Irancell_ExtraLight.woff2') format('woff2'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Irancell'; + src: url('/assets/fonts/Irancell_Light.woff2') format('woff2'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Irancell'; + src: url('/assets/fonts/Irancell_Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Irancell'; + src: url('/assets/fonts/Irancell_Medium.woff2') format('woff2'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Irancell'; + src: url('/assets/fonts/Irancell_Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Irancell'; + src: url('/assets/fonts/Irancell_ExtraBold.woff2') format('woff2'); + font-weight: 800; + font-style: normal; +} +/* ------------------ */ diff --git a/public/assets/fonts/Irancell_Bold.ttf b/public/assets/fonts/Irancell_Bold.ttf new file mode 100644 index 0000000..dac6c53 Binary files /dev/null and b/public/assets/fonts/Irancell_Bold.ttf differ diff --git a/public/assets/fonts/Irancell_Bold.woff b/public/assets/fonts/Irancell_Bold.woff new file mode 100644 index 0000000..8ab084e Binary files /dev/null and b/public/assets/fonts/Irancell_Bold.woff differ diff --git a/public/assets/fonts/Irancell_Bold.woff2 b/public/assets/fonts/Irancell_Bold.woff2 new file mode 100644 index 0000000..712665d Binary files /dev/null and b/public/assets/fonts/Irancell_Bold.woff2 differ diff --git a/public/assets/fonts/Irancell_ExtraBold.ttf b/public/assets/fonts/Irancell_ExtraBold.ttf new file mode 100644 index 0000000..01cfa2d Binary files /dev/null and b/public/assets/fonts/Irancell_ExtraBold.ttf differ diff --git a/public/assets/fonts/Irancell_ExtraBold.woff b/public/assets/fonts/Irancell_ExtraBold.woff new file mode 100644 index 0000000..d0c6a75 Binary files /dev/null and b/public/assets/fonts/Irancell_ExtraBold.woff differ diff --git a/public/assets/fonts/Irancell_ExtraBold.woff2 b/public/assets/fonts/Irancell_ExtraBold.woff2 new file mode 100644 index 0000000..19f6e38 Binary files /dev/null and b/public/assets/fonts/Irancell_ExtraBold.woff2 differ diff --git a/public/assets/fonts/Irancell_ExtraLight.ttf b/public/assets/fonts/Irancell_ExtraLight.ttf new file mode 100644 index 0000000..caf3ef6 Binary files /dev/null and b/public/assets/fonts/Irancell_ExtraLight.ttf differ diff --git a/public/assets/fonts/Irancell_ExtraLight.woff b/public/assets/fonts/Irancell_ExtraLight.woff new file mode 100644 index 0000000..9e6ba44 Binary files /dev/null and b/public/assets/fonts/Irancell_ExtraLight.woff differ diff --git a/public/assets/fonts/Irancell_ExtraLight.woff2 b/public/assets/fonts/Irancell_ExtraLight.woff2 new file mode 100644 index 0000000..f1cd9c0 Binary files /dev/null and b/public/assets/fonts/Irancell_ExtraLight.woff2 differ diff --git a/public/assets/fonts/Irancell_Light.ttf b/public/assets/fonts/Irancell_Light.ttf new file mode 100644 index 0000000..766ea21 Binary files /dev/null and b/public/assets/fonts/Irancell_Light.ttf differ diff --git a/public/assets/fonts/Irancell_Light.woff b/public/assets/fonts/Irancell_Light.woff new file mode 100644 index 0000000..39bbeec Binary files /dev/null and b/public/assets/fonts/Irancell_Light.woff differ diff --git a/public/assets/fonts/Irancell_Light.woff2 b/public/assets/fonts/Irancell_Light.woff2 new file mode 100644 index 0000000..d9e0b81 Binary files /dev/null and b/public/assets/fonts/Irancell_Light.woff2 differ diff --git a/public/assets/fonts/Irancell_Medium.ttf b/public/assets/fonts/Irancell_Medium.ttf new file mode 100644 index 0000000..1a31f82 Binary files /dev/null and b/public/assets/fonts/Irancell_Medium.ttf differ diff --git a/public/assets/fonts/Irancell_Medium.woff b/public/assets/fonts/Irancell_Medium.woff new file mode 100644 index 0000000..4d59799 Binary files /dev/null and b/public/assets/fonts/Irancell_Medium.woff differ diff --git a/public/assets/fonts/Irancell_Medium.woff2 b/public/assets/fonts/Irancell_Medium.woff2 new file mode 100644 index 0000000..a6e415a Binary files /dev/null and b/public/assets/fonts/Irancell_Medium.woff2 differ diff --git a/public/assets/fonts/Irancell_Regular.ttf b/public/assets/fonts/Irancell_Regular.ttf new file mode 100644 index 0000000..1fad271 Binary files /dev/null and b/public/assets/fonts/Irancell_Regular.ttf differ diff --git a/public/assets/fonts/Irancell_Regular.woff b/public/assets/fonts/Irancell_Regular.woff new file mode 100644 index 0000000..a2a9b06 Binary files /dev/null and b/public/assets/fonts/Irancell_Regular.woff differ diff --git a/public/assets/fonts/Irancell_Regular.woff2 b/public/assets/fonts/Irancell_Regular.woff2 new file mode 100644 index 0000000..77aebab Binary files /dev/null and b/public/assets/fonts/Irancell_Regular.woff2 differ diff --git a/public/assets/images/login-banner.png b/public/assets/images/login-banner.png new file mode 100644 index 0000000..cbbcb00 Binary files /dev/null and b/public/assets/images/login-banner.png differ diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx new file mode 100644 index 0000000..905c525 --- /dev/null +++ b/src/app/auth/page.tsx @@ -0,0 +1,221 @@ +'use client'; + +import React, { useState, type FormEvent, type ChangeEvent, useEffect, useRef } from 'react' +import PrimaryButton from '@/components/button/PrimaryButton' +import { EyeToggleIcon } from '@/components/icons/EyeToggleIcon'; +import InputField from '@/components/input/InputField'; +import OTPInputField from '@/components/input/MultiInputField'; + +enum LOGIN_STEPS { + ENTER_NUMBER = 0, + ENTER_PASSWORD = 1, + ENTER_OTP = 2, +} + +enum CLICK_ID { + TOGGLE_PASSWORD = "togglePassword", + RESET_PASSWORD = "resetPassword", + RESEND_OTP = "resendOtp" +} + +type Props = object + +function AuthIndex({ }: Props) { + const [number, setNumber] = useState(""); + const [password, setPassword] = useState(""); + const [otp, setOtp] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [rememberMe, setRememberMe] = useState(false); + const [step, setStep] = useState(LOGIN_STEPS.ENTER_NUMBER); + const [timerRunning, setTimerRunning] = useState(false); + const [secondLeft, setSecondsLeft] = useState(0); + const intervalRef = useRef(null); // ← useRef for interval + + useEffect(() => { + if (timerRunning && intervalRef.current === null) { + intervalRef.current = setInterval(() => { + setSecondsLeft((state) => { + if (state <= 1) { // changed to <= 1 to prevent negative values + setTimerRunning(false); + clearInterval(intervalRef.current!); + intervalRef.current = null; + return 0; + } + return state - 1; + }); + }, 1000); + } + + return () => { + // Cleanup if component unmounts or timerRunning changes + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [timerRunning]); + + + useEffect(() => { + if (step == LOGIN_STEPS.ENTER_OTP) { + setSecondsLeft(30); + setTimerRunning(true); + } + return () => { + + } + }, [step]) + + const updateInput = (e: ChangeEvent) => { + if (e.target.id == "phone") { + setNumber(() => e.target.value) + } + else if (e.target.id == "password") { + setPassword(() => e.target.value) + } + else if (e.target.id == "rememberMe") { + setRememberMe((state) => !state) + } + else if (e.target.id.startsWith("otp-data")) { + const index = +e.target.id.split('-')[2]; + const prev = otp.padEnd(6).split(''); + prev[index] = e.target.value; + const next = prev.join(''); + setOtp(next.trimEnd()); + } + } + + const onClickEvent = (e: React.MouseEvent) => { + e.stopPropagation(); + const target = e.target as HTMLButtonElement; + + if (target.id === CLICK_ID.TOGGLE_PASSWORD) { + setShowPassword((state) => !state); + } + else if (target.id === CLICK_ID.RESET_PASSWORD) { + setShowPassword((state) => !state); + } + else if (target.id === CLICK_ID.RESEND_OTP) { + setSecondsLeft(30); + setTimerRunning(true); + } + }; + + const submit = (e: FormEvent) => { + e.preventDefault(); + console.log("STEP: ", step) + if (step == LOGIN_STEPS.ENTER_NUMBER) { + setStep(LOGIN_STEPS.ENTER_PASSWORD) + } + if (step == LOGIN_STEPS.ENTER_PASSWORD) { + setStep(LOGIN_STEPS.ENTER_OTP) + } + if (step == LOGIN_STEPS.ENTER_OTP) { + setStep(LOGIN_STEPS.ENTER_NUMBER) + setTimerRunning(false); + } + } + + if (step == LOGIN_STEPS.ENTER_NUMBER) { + // First Step -> Enter number + return ( +
+
+ +
+
ورود به سیستم
+

برای ورود شماره همراه خود را وارد نمایید.

+
+ +
+ بعدی +
+ ) + } + + if (step == LOGIN_STEPS.ENTER_PASSWORD) { + return ( +
+
+ +
+
ورود به سیستم
+

کلمه عبور خود را وارد نمایید.

+
+ + + +
+ + +
+ {/* TODO: customize checkbox */} + +
+
+
+ ورود به منو +
+ ) + } + + if (step == LOGIN_STEPS.ENTER_OTP) { + return ( +
+
+ +
+ {/* // TODO: add persian digits font */} +
کد فعالسازی را وارد کنید
+

کد ۶ رقمی ارسال شده به شماره {number} را وارد نمایید.

+
+ + + +
+
+ {timerRunning ? +
{secondLeft} ثانیه دیگر تا دریافت کد
: +
+ کد را دریافت نکردید؟ + + + +
+ } +
+
+
+ بعدی +
+ ) + } +} + +export default AuthIndex \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..7903e3f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,64 @@ @import "tailwindcss"; +@import "../../public/assets/css/fonts.css"; -:root { - --background: #ffffff; - --foreground: #171717; +@theme { + --font-irancell: "Irancell"; + + --color-background: #FFFFFF; + --color-primary: #C39854; + --color-foreground: #333333; + --color-disabled: #999999; + --radius-normal: 10px; + --text-sm2: 13px; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); +* { + box-sizing: border-box; } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +html, +body { + @apply h-full bg-background font-irancell font-normal text-foreground; + + @media (prefers-color-scheme: dark) { + /* color-scheme: dark; */ } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +/* Chrome, Safari, Edge, Opera */ +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; } + +/* Firefox */ +input[type="number"] { + -moz-appearance: textfield; +} + +input[type="password"] { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + color: var(--color-disabled); + margin: 10px 0px 10px 0px; + font-size: 34px; + /* Controls dot size (width & height) */ + line-height: 8px; + /* Ensures vertical alignment */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ + + /* add spacing to better separate each image */ + letter-spacing: 2px; +} \ No newline at end of file diff --git a/src/components/button/PrimaryButton.tsx b/src/components/button/PrimaryButton.tsx new file mode 100644 index 0000000..6f9a304 --- /dev/null +++ b/src/components/button/PrimaryButton.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +type Props = { } & React.ButtonHTMLAttributes; + +function PrimaryButton({children, ...rest}: Props) { + return ( + + ) +} + +export default PrimaryButton \ No newline at end of file diff --git a/src/components/icons/EyeToggleIcon.tsx b/src/components/icons/EyeToggleIcon.tsx new file mode 100644 index 0000000..acdfd47 --- /dev/null +++ b/src/components/icons/EyeToggleIcon.tsx @@ -0,0 +1,70 @@ +import { motion } from "framer-motion"; +import React from "react"; + +type EyeToggleIconProps = { + slash: boolean; +} & React.SVGProps; + +export const EyeToggleIcon: React.FC = ({ slash, ...props }) => { + return ( + + + + + + + {/* Eye Circle */} + + + {/* Eye Outline */} + + + {/* Slash Line - Animate with Framer Motion */} + + + + ); +}; diff --git a/src/components/input/InputField.tsx b/src/components/input/InputField.tsx new file mode 100644 index 0000000..336ad1d --- /dev/null +++ b/src/components/input/InputField.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +type Props = { + htmlFor: string; + labelText?: React.ReactNode; + value?: string; + onChange: (e: React.ChangeEvent) => void; +} & Omit, "id">; + + +function InputField({ onChange, htmlFor, labelText, children, className, ...inputProps }: Props) { + return ( +
+ + + {children} +
+ ) +} + +export default InputField \ No newline at end of file diff --git a/src/components/input/MultiInputField.tsx b/src/components/input/MultiInputField.tsx new file mode 100644 index 0000000..7d2e9fa --- /dev/null +++ b/src/components/input/MultiInputField.tsx @@ -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) => void; + maxLength?: number; + className?: string; +} & Omit, "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())); + 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) => { + 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 ( +
+ +
+ {inputRefs?.current?.map((ref, i) => { + return ( + + ) + })} +
+
+ ) +} + +export default OTPInputField \ No newline at end of file