diff --git a/app/src/app.tsx b/app/src/app.tsx index b03d4df..9685c13 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -6,6 +6,7 @@ import Project from "./pages/Project"; import UserAuth from "./pages/UserAuth"; import "./styles/main.scss"; import { LoggerProvider } from "./components/ui/log/LoggerContext"; +import ForgotPassword from "./pages/ForgotPassword"; const App: React.FC = () => { @@ -19,8 +20,9 @@ const App: React.FC = () => { } /> + } /> } /> - } /> + } /> diff --git a/app/src/assets/cursors/close.svg b/app/src/assets/cursors/close.svg index 6fdebed..5b809eb 100644 --- a/app/src/assets/cursors/close.svg +++ b/app/src/assets/cursors/close.svg @@ -1,6 +1,6 @@ - + diff --git a/app/src/assets/cursors/pointing.svg b/app/src/assets/cursors/pointing.svg index 4a50a4b..1daa471 100644 --- a/app/src/assets/cursors/pointing.svg +++ b/app/src/assets/cursors/pointing.svg @@ -1,6 +1,6 @@ - + diff --git a/app/src/assets/image/categories/decal.png b/app/src/assets/image/categories/decal.png new file mode 100644 index 0000000..ece13a7 Binary files /dev/null and b/app/src/assets/image/categories/decal.png differ diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 0f75d19..8ac05b6 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -75,7 +75,7 @@ const DashboardCard: React.FC = ({ setLoadingProgress(1); setProjectName(projectName); - navigate(`/${projectId}`); + navigate(`/projects/${projectId}`); } catch {} }; @@ -108,7 +108,7 @@ const DashboardCard: React.FC = ({ setIsKebabOpen(false); } } catch (error) {} - window.open(`/${projectId}`, "_blank"); + window.open(`/projects/${projectId}`, "_blank"); break; case "rename": setIsRenaming(true); diff --git a/app/src/components/forgotPassword/ConfirmationMessgae.tsx b/app/src/components/forgotPassword/ConfirmationMessgae.tsx new file mode 100644 index 0000000..4a22b03 --- /dev/null +++ b/app/src/components/forgotPassword/ConfirmationMessgae.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { SuccessIcon } from '../icons/ExportCommonIcons'; + +const ConfirmationMessage: React.FC = () => { + return ( + + + Successfully + Your password has been reset successfully + Login + + ); +}; + +export default ConfirmationMessage; diff --git a/app/src/components/forgotPassword/EmailInput.tsx b/app/src/components/forgotPassword/EmailInput.tsx new file mode 100644 index 0000000..d3ca158 --- /dev/null +++ b/app/src/components/forgotPassword/EmailInput.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +interface Props { + email: string; + setEmail: (value: string) => void; + onSubmit: () => void; +} + +const EmailInput: React.FC = ({ email, setEmail, onSubmit }) => { + return ( + + Forgot password + + Enter your email for the verification process, we will send a 4-digit code to your email. + + { e.preventDefault(); onSubmit(); }}> + setEmail(e.target.value)} + required + /> + Continue + + + ); +}; + +export default EmailInput; diff --git a/app/src/components/forgotPassword/OTPInput.tsx b/app/src/components/forgotPassword/OTPInput.tsx new file mode 100644 index 0000000..e14b45e --- /dev/null +++ b/app/src/components/forgotPassword/OTPInput.tsx @@ -0,0 +1,52 @@ +import React, { useState, useRef, useEffect } from 'react'; + +const OTPInput: React.FC<{ length?: number; onComplete: (otp: string) => void }> = ({ length = 4, onComplete }) => { + const [otpValues, setOtpValues] = useState(Array(length).fill('')); + const inputsRef = useRef<(HTMLInputElement | null)[]>([]); + + // Auto focus first input on mount + useEffect(() => { + inputsRef.current[0]?.focus(); + }, []); + + const handleChange = (value: string, index: number) => { + if (/^[0-9]?$/.test(value)) { + const newOtp = [...otpValues]; + newOtp[index] = value; + setOtpValues(newOtp); + + if (value && index < length - 1) { + inputsRef.current[index + 1]?.focus(); + } + + if (newOtp.every((digit) => digit !== '')) { + onComplete(newOtp.join('')); + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent, index: number) => { + if (e.key === 'Backspace' && !otpValues[index] && index > 0) { + inputsRef.current[index - 1]?.focus(); + } + }; + + return ( + + {otpValues.map((value, index) => ( + handleChange(e.target.value, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + ref={(el) => (inputsRef.current[index] = el)} + /> + ))} + + ); +}; + +export default OTPInput; diff --git a/app/src/components/forgotPassword/OTP_Verification.tsx b/app/src/components/forgotPassword/OTP_Verification.tsx new file mode 100644 index 0000000..8af7836 --- /dev/null +++ b/app/src/components/forgotPassword/OTP_Verification.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import OTPInput from './OTPInput'; + +interface Props { + email: string; + timer: number; + setCode: (value: string) => void; + onSubmit: () => void; + resendCode: () => void; +} + +const OTPVerification: React.FC = ({ email, timer, setCode, onSubmit, resendCode }) => { + const [otp, setOtp] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log('otp.length: ', otp.length); + if (otp.length === 4) { + onSubmit(); + } else { + alert('Please enter the 4-digit code'); + } + }; + + return ( + + Verification + + Enter the 4-digit code sent to {email}. + + + { setOtp(code); setCode(code); }} /> + + {timer > 0 + ? `${String(Math.floor(timer / 60)).padStart(2, '0')}:${String(timer % 60).padStart(2, '0')}` + : ''} + + + Verify + + + 0 ? 'disabled' : ''}`} + onClick={timer === 0 ? resendCode : undefined} + style={{ cursor: timer === 0 ? 'pointer' : 'not-allowed', opacity: timer === 0 ? 1 : 0.5 }} + > + If you didn’t receive a code, Resend + + + ); +}; + +export default OTPVerification; diff --git a/app/src/components/forgotPassword/PasswordSetup.tsx b/app/src/components/forgotPassword/PasswordSetup.tsx new file mode 100644 index 0000000..4c6af1c --- /dev/null +++ b/app/src/components/forgotPassword/PasswordSetup.tsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import { EyeIcon } from '../icons/ExportCommonIcons'; + +interface Props { + newPassword: string; + confirmPassword: string; + setNewPassword: (value: string) => void; + setConfirmPassword: (value: string) => void; + onSubmit: () => void; +} + +const PasswordSetup: React.FC = ({ + newPassword, + confirmPassword, + setNewPassword, + setConfirmPassword, + onSubmit +}) => { + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + return ( + + New Password + Set the new password for your account so you can login and access all features. + { + e.preventDefault(); + if (newPassword !== confirmPassword) { + alert('Passwords do not match'); + return; + } + onSubmit(); + }} + > + + setNewPassword(e.target.value)} + required + /> + setShowNewPassword(prev => !prev)} + > + + + + + + setConfirmPassword(e.target.value)} + required + /> + setShowConfirmPassword(prev => !prev)} + > + + + + + Update password + + + ); +}; + +export default PasswordSetup; diff --git a/app/src/components/icons/ContextMenuIcons.tsx b/app/src/components/icons/ContextMenuIcons.tsx index 210f1b9..3220547 100644 --- a/app/src/components/icons/ContextMenuIcons.tsx +++ b/app/src/components/icons/ContextMenuIcons.tsx @@ -192,7 +192,7 @@ export function DublicateIcon() { - + @@ -278,7 +278,7 @@ export function MoveIcon() { export function RotateIcon() { return ( - + ); } @@ -286,7 +286,7 @@ export function RotateIcon() { export function GroupIcon() { return ( - + ); } diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 6f9a1fb..7cee77f 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -1335,3 +1335,14 @@ export const GreenTickIcon = () => { ); }; + + +export const SuccessIcon = () => { + return ( + + + + + + ) +} \ No newline at end of file diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx index 8c8b4d7..9ea5f1a 100644 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ b/app/src/components/layout/sidebarLeft/Assets.tsx @@ -13,6 +13,7 @@ import storage from "../../../assets/image/categories/storage.png"; import office from "../../../assets/image/categories/office.png"; import safety from "../../../assets/image/categories/safety.png"; import feneration from "../../../assets/image/categories/feneration.png"; +import decal from "../../../assets/image/categories/decal.png"; import SkeletonUI from "../../templates/SkeletonUI"; // ------------------------------------- @@ -86,6 +87,7 @@ const Assets: React.FC = () => { useEffect(() => { setCategoryList([ { category: "Fenestration", categoryImage: feneration }, + { category: "Decals", categoryImage: decal }, { category: "Vehicles", categoryImage: vehicle }, { category: "Workstation", categoryImage: workStation }, { category: "Machines", categoryImage: machines }, diff --git a/app/src/pages/ForgotPassword.tsx b/app/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..8681996 --- /dev/null +++ b/app/src/pages/ForgotPassword.tsx @@ -0,0 +1,92 @@ +import React, { useState, useEffect } from 'react'; +import { LogoIconLarge } from '../components/icons/Logo'; +import EmailInput from '../components/forgotPassword/EmailInput'; +import OTPVerification from '../components/forgotPassword/OTP_Verification'; +import PasswordSetup from '../components/forgotPassword/PasswordSetup'; +import ConfirmationMessage from '../components/forgotPassword/ConfirmationMessgae'; + +const ForgotPassword: React.FC = () => { + const [step, setStep] = useState(1); + const [email, setEmail] = useState(''); + const [code, setCode] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [timer, setTimer] = useState(30); + + useEffect(() => { + let countdown: NodeJS.Timeout; + if (step === 2 && timer > 0) { + countdown = setTimeout(() => setTimer(prev => prev - 1), 1000); + } + return () => clearTimeout(countdown); + }, [step, timer]); + + const handleSubmitEmail = () => { + setStep(2); + setTimer(30); + } + const resendCode = () => { + // TODO: call API to resend code + setTimer(30); + }; + + return ( + + + + + + + + {step === 1 && ( + <> + + Login + > + + )} + + + {step === 2 && ( + <> + setStep(3)} + resendCode={resendCode} + /> + Login + > + + )} + + + {step === 3 && ( + <> + setStep(4)} + /> + Login + > + + )} + + + {step === 4 && } + + + + + ); +}; + +export default ForgotPassword; diff --git a/app/src/pages/UserAuth.tsx b/app/src/pages/UserAuth.tsx index fe65701..8c45b77 100644 --- a/app/src/pages/UserAuth.tsx +++ b/app/src/pages/UserAuth.tsx @@ -58,10 +58,10 @@ const UserAuth: React.FC = () => { const projects = await recentlyViewed(organization, res.message.userId); if (res.message.isShare) { if (Object.values(projects.RecentlyViewed).length > 0) { - const firstId = (Object.values(projects?.RecentlyViewed || {})[0] as any)?._id; - if (Object.values(projects?.RecentlyViewed).filter((val: any) => val._id == firstId)) { + const recent_opend_projectID = (Object.values(projects?.RecentlyViewed || {})[0] as any)?._id; + if (Object.values(projects?.RecentlyViewed).filter((val: any) => val._id == recent_opend_projectID)) { setLoadingProgress(1) - navigate(`/${firstId}`) + navigate(`/projects/${recent_opend_projectID}`) } else { navigate("/Dashboard") } @@ -200,6 +200,9 @@ const UserAuth: React.FC = () => { + + {isSignIn && Forgot password ?} + {!isSignIn && ( diff --git a/app/src/styles/base/reset.scss b/app/src/styles/base/reset.scss index 82d286e..abd16f7 100644 --- a/app/src/styles/base/reset.scss +++ b/app/src/styles/base/reset.scss @@ -6,9 +6,17 @@ font-size: var(--font-size-regular); } -input[type="password"]::-ms-reveal, /* For Microsoft Edge */ -input[type="password"]::-ms-clear, /* For Edge clear button */ -input[type="password"]::-webkit-clear-button, /* For Chrome/Safari clear button */ -input[type="password"]::-webkit-inner-spin-button { /* Just in case */ +input[type="password"]::-ms-reveal, +/* For Microsoft Edge */ +input[type="password"]::-ms-clear, +/* For Edge clear button */ +input[type="password"]::-webkit-clear-button, +/* For Chrome/Safari clear button */ +input[type="password"]::-webkit-inner-spin-button { + /* Just in case */ display: none; } + +a { + text-decoration: none; +} \ No newline at end of file diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index 6b24256..9030be7 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -1657,11 +1657,12 @@ .sidebar-right-wrapper { .wall-properties-container { - .wall-properties-section{ + .wall-properties-section { padding: 14px; padding-bottom: 0; margin-bottom: 8px; } + .header { color: var(--text-color); } @@ -1910,63 +1911,59 @@ &:nth-child(1), &:nth-child(8) { &::after { - @include gradient-by-child( - 1 - ); // First child uses the first color + @include gradient-by-child(1); // First child uses the first color } } - &:nth-child(2), + &:nth-child(2) { + &::after { + + // @include gradient-by-child(4); // Second child uses the second color + background: linear-gradient(144.19deg, rgba(197, 137, 26, 0.7) 16.62%, rgba(69, 48, 10, 0.7) 85.81%); + + } + + } + + &:nth-child(3), &:nth-child(9) { &::after { - @include gradient-by-child( - 2 - ); // Second child uses the second color + @include gradient-by-child(3); // Second child uses the second color } } &:nth-child(3), &:nth-child(10) { &::after { - @include gradient-by-child( - 3 - ); // Third child uses the third color + @include gradient-by-child(3); // Third child uses the third color } } &:nth-child(4), &:nth-child(11) { &::after { - @include gradient-by-child( - 4 - ); // Fourth child uses the fourth color + @include gradient-by-child(4); // Fourth child uses the fourth color } } &:nth-child(5), &:nth-child(12) { &::after { - @include gradient-by-child( - 5 - ); // Fifth child uses the fifth color + @include gradient-by-child(5); // Fifth child uses the fifth color } } &:nth-child(6), &:nth-child(13) { &::after { - @include gradient-by-child( - 6 - ); // Fifth child uses the fifth color + @include gradient-by-child(6); // Fifth child uses the fifth color } } &:nth-child(7), &:nth-child(14) { &::after { - @include gradient-by-child( - 7 - ); // Fifth child uses the fifth color + @include gradient-by-child(7); // Fifth child uses the fifth color } } @@ -2025,11 +2022,9 @@ width: 100%; height: 100%; font-size: var(--font-size-regular); - background: linear-gradient( - 0deg, - rgba(37, 24, 51, 0) 0%, - rgba(52, 41, 61, 0.5) 100% - ); + background: linear-gradient(0deg, + rgba(37, 24, 51, 0) 0%, + rgba(52, 41, 61, 0.5) 100%); pointer-events: none; backdrop-filter: blur(8px); opacity: 0; @@ -2285,4 +2280,4 @@ text-transform: capitalize; } } -} +} \ No newline at end of file diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index 69dc2b7..b73439f 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -43,6 +43,7 @@ @use "pages/home"; @use "pages/realTimeViz"; @use "pages/userAuth"; +@use "pages/forgotPassword"; // @use "./scene/scene"; diff --git a/app/src/styles/pages/forgotPassword.scss b/app/src/styles/pages/forgotPassword.scss new file mode 100644 index 0000000..4965029 --- /dev/null +++ b/app/src/styles/pages/forgotPassword.scss @@ -0,0 +1,100 @@ +@use "../abstracts/variables.scss" as *; +@use "../abstracts/mixins.scss" as *; + +.forgot-password-page { + + .header, + .sub-header { + margin: 0; + text-align: center; + line-height: 20px; + } + + .login { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 350px; + padding: 10px; + margin-bottom: 20px; + border: 1px solid var(--highlight-text-color); + border-radius: 20px; + background: transparent; + color: var(--highlight-text-color); + font-size: 14px; + outline: none; + cursor: pointer; + } + + .forgot-password-wrapper { + width: 25%; + display: flex; + flex-direction: column; + gap: 16px; + justify-content: center; + align-items: center; + + .logo-icon { + width: 150px; + height: auto; + margin: auto; + } + + .request-container { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 16px; + + .sub-header { + width: 100% !important; + } + + .login { + position: relative; + top: 0; + left: 0; + } + + .auth-form { + margin: 0; + + .continue-button { + margin-top: 6px; + } + + .timing { + margin: 6px 0; + color: #F2451C; + + } + + .otp-container { + display: flex; + gap: 24px; + justify-content: center; + + .otp-input { + width: 60px !important; + height: 60px !important; + text-align: center; + font-size: 20px; + font-weight: bold; + border: 1px solid var(--border-color); + border-radius: 4px !important; + outline: none; + transition: border-color 0.2s; + + &:focus, + &:active { + outline: 1px solid var(--border-color-accent); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/styles/pages/userAuth.scss b/app/src/styles/pages/userAuth.scss index e21c401..46c9b2e 100644 --- a/app/src/styles/pages/userAuth.scss +++ b/app/src/styles/pages/userAuth.scss @@ -119,6 +119,17 @@ } } + .forgot-password { + width: 100%; + margin-bottom: 10px; + margin-right: 12px; + color: var(--text-color); + font-size: 12px; + text-align: right; + cursor: pointer; + text-decoration: none; + } + .continue-button { width: 100%; padding: 10px;
Your password has been reset successfully
+ Enter your email for the verification process, we will send a 4-digit code to your email. +
+ Enter the 4-digit code sent to {email}. +
Set the new password for your account so you can login and access all features.