diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..2a70607 Binary files /dev/null and b/.DS_Store differ diff --git a/compass/app/auth/forgot_password/page.tsx b/compass/app/auth/forgot_password/page.tsx new file mode 100644 index 0000000..555f0cd --- /dev/null +++ b/compass/app/auth/forgot_password/page.tsx @@ -0,0 +1,58 @@ +// pages/forgot-password.tsx +"use client"; + +import React, { useState, useEffect } from 'react'; +import Input from '@/components/Input'; +import Button from '@/components/Button'; +import InlineLink from '@/components/InlineLink'; +import Paper from '@/components/auth/Paper'; +import ErrorBanner from '@/components/auth/ErrorBanner'; + + +export default function ForgotPasswordPage() { + const [confirmEmail, setConfirmEmail] = useState(""); + const [emailError, setEmailError] = useState(null); + + + function isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (email.trim() === '') { + setEmailError('Email cannot be empty'); + return false; + } else if (!emailRegex.test(email)) { + setEmailError('Invalid email format'); + return false; + } + return true; // No error + } + const handleClick = (event: React.MouseEvent) => { + isValidEmail(confirmEmail); + event.preventDefault(); + } + + return ( + <> +

Forgot Password

+
+ setConfirmEmail(e.target.value)} + /> +
+ {emailError && } +
+ + Back to Sign In + + +
+ + + ); +} diff --git a/compass/app/auth/layout.tsx b/compass/app/auth/layout.tsx new file mode 100644 index 0000000..6a6fb96 --- /dev/null +++ b/compass/app/auth/layout.tsx @@ -0,0 +1,22 @@ + +import Paper from '@/components/auth/Paper'; + + +export default function RootLayout({ + // Layouts must accept a children prop. + // This will be populated with nested layouts or pages + children, +}: { + children: React.ReactNode +}) { + return ( + +
+ {children} +
+

+ © 2024 Compass Center +

+
+ ) +} \ No newline at end of file diff --git a/compass/app/auth/login/page.tsx b/compass/app/auth/login/page.tsx new file mode 100644 index 0000000..5055714 --- /dev/null +++ b/compass/app/auth/login/page.tsx @@ -0,0 +1,82 @@ +// pages/index.tsx +"use client"; + +import Button from '@/components/Button'; +import Input from '@/components/Input' +import InlineLink from '@/components/InlineLink'; +import Paper from '@/components/auth/Paper'; +import Image from 'next/image'; +import { useState } from "react"; +import PasswordInput from '@/components/auth/PasswordInput'; +import ErrorBanner from '@/components/auth/ErrorBanner'; + +export default function Page() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.currentTarget.value); + } + + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.currentTarget.value); + } + + const handleClick = (event: React.MouseEvent) => { + // Priority: Incorrect combo > Missing email > Missing password + + if (password.trim().length === 0) { + setEmailError("Please enter your password.") + event.preventDefault(); + } + // This shouldn't happen, already provides validation, but just in case. + if (email.trim().length === 0) { + setPasswordError("Please enter your email.") + event.preventDefault(); + } + // Placeholder for incorrect email + password combo. + if (email === "incorrect@gmail.com" && password) { + setPasswordError("Incorrect password.") + event.preventDefault(); + } + } + + return ( + <> + Compass Center logo. + +

Login

+ +
+ + +
+ {emailError && } + +
+ + +
+ {passwordError && } + +
+ + Forgot password? + + +
+ + + + ); +}; + diff --git a/compass/app/auth/new_password/page.tsx b/compass/app/auth/new_password/page.tsx new file mode 100644 index 0000000..3fb3e83 --- /dev/null +++ b/compass/app/auth/new_password/page.tsx @@ -0,0 +1,64 @@ +// pages/index.tsx +"use client"; +import { useState, useEffect } from 'react'; +import Button from '@/components/Button'; + +import Paper from '@/components/auth/Paper'; +import PasswordInput from '@/components/auth/PasswordInput'; +import ErrorBanner from '@/components/auth/ErrorBanner'; + + +function isStrongPassword(password: string): boolean { + const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/; + return strongPasswordRegex.test(password); +} + + +export default function Page() { + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + + useEffect(() => { + setIsButtonDisabled(newPassword === '' || confirmPassword === '' || newPassword !== confirmPassword|| !isStrongPassword(newPassword)); + }, [newPassword, confirmPassword]); + + + return ( + <> +
+

New Password

+
+
+ { + setNewPassword(e.target.value); + }} + /> +
+ {isStrongPassword(newPassword) || newPassword === '' ? null : } +
+ { + setConfirmPassword(e.target.value); + }} + /> +
+ {newPassword === confirmPassword || confirmPassword === '' ? null : } +
+ +
+ + ); +} + + + diff --git a/compass/app/page.tsx b/compass/app/page.tsx index 756075c..dba820a 100644 --- a/compass/app/page.tsx +++ b/compass/app/page.tsx @@ -1,33 +1,79 @@ // pages/index.tsx +"use client"; import Button from '@/components/Button'; import Input from '@/components/Input' import InlineLink from '@/components/InlineLink'; import Paper from '@/components/auth/Paper'; -import { Metadata } from 'next' - -export const metadata: Metadata = { - title: 'Login', -} +// import { Metadata } from 'next' +import Image from 'next/image'; +import {ChangeEvent, useState} from "react"; + +// export const metadata: Metadata = { +// title: 'Login', +// } export default function Page() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.currentTarget.value); + console.log("email " + email); + } + + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.currentTarget.value); + console.log("password " + password) + } + + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + // Priority: Incorrect combo > Missing email > Missing password + + if (password.trim().length === 0) { + setError("Please enter your password.") + } + // This shouldn't happen, already provides validation, but just in case. + if (email.trim().length === 0) { + setError("Please enter your email.") + } + // Placeholder for incorrect email + password combo. + if (email === "incorrect@gmail.com" && password) { + setError("Incorrect password.") + } + } + + + return ( <> -
+ + Compass Center logo. +

Login

- +
- +
- + Forgot password? - +
@@ -37,5 +83,4 @@ export default function Page() {
); -}; - +}; \ No newline at end of file diff --git a/compass/components/Button.tsx b/compass/components/Button.tsx index 72c47f9..0ebfe6c 100644 --- a/compass/components/Button.tsx +++ b/compass/components/Button.tsx @@ -2,22 +2,25 @@ import { FunctionComponent, ReactNode } from 'react'; type ButtonProps = { children: ReactNode; - onClick?: () => void; // make the onClick handler optional + onClick?: (event: React.MouseEvent) => void; + type?: "button" | "submit" | "reset"; // specify possible values for type + disabled?: boolean; }; -const Button: FunctionComponent = ({ children, onClick }) => { + +const Button: FunctionComponent = ({ children, type, disabled, onClick}) => { + const buttonClassName = `inline-block rounded border ${disabled ? 'bg-gray-400 text-gray-600 cursor-not-allowed' : 'border-purple-600 bg-purple-600 text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500'} px-4 py-1 text-md font-semibold w-20 h-10 text-center`; + return ( ); }; + export default Button; diff --git a/compass/components/InlineLink.tsx b/compass/components/InlineLink.tsx index a913ff4..f2e45ef 100644 --- a/compass/components/InlineLink.tsx +++ b/compass/components/InlineLink.tsx @@ -7,7 +7,7 @@ interface Link { const InlineLink: React.FC = ({href = '#', children}) => { return ( - + {children} ) diff --git a/compass/components/Input.tsx b/compass/components/Input.tsx index 5997296..d7fafc7 100644 --- a/compass/components/Input.tsx +++ b/compass/components/Input.tsx @@ -1,41 +1,36 @@ -import { Icons } from '@/utils/constants'; -import React, { FunctionComponent, InputHTMLAttributes, ReactElement, ReactNode } from 'react'; +import React, { FunctionComponent, InputHTMLAttributes, ReactNode, ChangeEvent } from 'react'; type InputProps = InputHTMLAttributes & { - iconKey?: keyof typeof Icons; // Use keyof typeof to ensure the key exists in Icons - title?: string; // Assuming title is always a string - type?: string; - placeholder?: string; + icon?: ReactNode; + title?: ReactNode; + type?:ReactNode; + placeholder?:ReactNode + valid?:boolean; + onChange: (event: ChangeEvent) => void; }; -const Input: FunctionComponent = ({ iconKey, type, title, placeholder, ...rest }) => { - const IconComponent = iconKey ? Icons[iconKey] : null; - +const Input: FunctionComponent = ({ icon, type, title, placeholder, onChange, valid = true, ...rest }) => { return ( -
- {title && ( -
- -
- )} -
- {IconComponent && ( - - - - )} - -
-
+
+ +
); }; diff --git a/compass/components/auth/ErrorBanner.tsx b/compass/components/auth/ErrorBanner.tsx new file mode 100644 index 0000000..c7dfea3 --- /dev/null +++ b/compass/components/auth/ErrorBanner.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +interface ErrorBannerProps { + heading: string; + description?: string | null; +} + +const ErrorBanner: React.FC = ({ heading, description = null }) => { + return ( +
+ {heading} + {description &&

+ {description} +

} +
+ ); +}; + +export default ErrorBanner; diff --git a/compass/components/auth/PasswordInput.tsx b/compass/components/auth/PasswordInput.tsx new file mode 100644 index 0000000..c7f3b70 --- /dev/null +++ b/compass/components/auth/PasswordInput.tsx @@ -0,0 +1,35 @@ +import React, { useState, FunctionComponent, ChangeEvent, ReactNode } from 'react'; +import Input from '../Input'; // Adjust the import path as necessary +import { Icons } from '@/utils/constants'; + +type PasswordInputProps = { + title?: ReactNode; // Assuming you might want to reuse title, placeholder etc. + placeholder?: ReactNode; + valid?: boolean; + onChange: (event: ChangeEvent) => void; +}; + +const PasswordInput: FunctionComponent = ({ onChange, valid = true, ...rest }) => { + const [visible, setVisible] = useState(false); + + const toggleVisibility = () => { + setVisible(!visible); + }; + + const PasswordIcon = visible ? Icons['HidePasswordIcon'] : Icons['UnhidePasswordIcon']; + + // Render the Input component and pass the PasswordIcon as an icon prop + return ( + + } + /> + ); +}; + +export default PasswordInput; diff --git a/compass/public/logo.png b/compass/public/logo.png new file mode 100644 index 0000000..6ab7af4 Binary files /dev/null and b/compass/public/logo.png differ diff --git a/compass/tsconfig.json b/compass/tsconfig.json index c714696..1acc222 100644 --- a/compass/tsconfig.json +++ b/compass/tsconfig.json @@ -8,7 +8,7 @@ "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "bundler", + "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", diff --git a/compass/utils/classes/InputProps.ts b/compass/utils/classes/InputProps.ts new file mode 100644 index 0000000..2354d62 --- /dev/null +++ b/compass/utils/classes/InputProps.ts @@ -0,0 +1,9 @@ +import { InputHTMLAttributes } from "react"; +import { Icons } from "../constants"; + +export type InputProps = InputHTMLAttributes & { + iconKey?: keyof typeof Icons; // Use keyof typeof to ensure the key exists in Icons + title?: string; // Assuming title is always a string + type?: string; + placeholder?: string; + }; \ No newline at end of file