Merge pull request #18 from cssgunc/admin-team-GEN-67-auth-app

Completed the UI for the auth portion of the app
This commit is contained in:
Meliora Ho 2024-03-22 14:02:28 -04:00 committed by GitHub
commit 3e6875cffe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 388 additions and 56 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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<string | null>(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<HTMLButtonElement>) => {
isValidEmail(confirmEmail);
event.preventDefault();
}
return (
<>
<h1 className="font-bold text-xl text-purple-800">Forgot Password</h1>
<div className="mb-6">
<Input
type='email'
valid={emailError == null}
title="Enter your email address"
placeholder="janedoe@gmail.com"
value={confirmEmail}
onChange={(e) => setConfirmEmail(e.target.value)}
/>
</div>
{emailError && <ErrorBanner heading={emailError} />}
<div className="flex flex-col items-left space-y-4">
<InlineLink href="/auth/login">
Back to Sign In
</InlineLink>
<Button type="submit" onClick={handleClick}>
Send
</Button>
</div>
</>
);
}

View File

@ -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 (
<Paper>
<form className="mb-0 m-auto mt-6 space-y-4 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl">
{children}
</form>
<p className="text-center mt-6 text-gray-500 text-xs">
&copy; 2024 Compass Center
</p>
</Paper>
)
}

View File

@ -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<HTMLInputElement>) => {
setEmail(event.currentTarget.value);
}
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.currentTarget.value);
}
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Priority: Incorrect combo > Missing email > Missing password
if (password.trim().length === 0) {
setEmailError("Please enter your password.")
event.preventDefault();
}
// This shouldn't happen, <input type="email"> 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 (
<>
<Image
src="/logo.png"
alt='Compass Center logo.'
width={100}
height={91}
/>
<h1 className='font-bold text-xl text-purple-800'>Login</h1>
<div className="mb-6">
<Input type='email' valid={emailError == ""} title="Email" placeholder="janedoe@gmail.com" onChange={handleEmailChange} required />
</div>
{emailError && <ErrorBanner heading={emailError} />}
<div className="mb-6">
<PasswordInput title="Password" valid={passwordError == ""} onChange={handlePasswordChange} required />
</div>
{passwordError && <ErrorBanner heading={passwordError} />}
<div className="flex flex-col items-left space-y-4">
<InlineLink href="/auth/forgot_password">
Forgot password?
</InlineLink>
<Button onClick={handleClick}>
Login
</Button>
</div>
</>
);
};

View File

@ -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 (
<>
<div className="text-center sm:text-left">
<h1 className="font-bold text-xl text-purple-800">New Password</h1>
</div>
<div className="mb-4">
<PasswordInput
title="Enter New Password"
value={newPassword}
valid={!isButtonDisabled || isStrongPassword(newPassword)}
onChange={(e) => {
setNewPassword(e.target.value);
}}
/>
</div>
{isStrongPassword(newPassword) || newPassword === '' ? null : <ErrorBanner heading="Password is not strong enough." description="Tip: Use a mix of letters, numbers, and symbols for a strong password. Aim for at least 8 characters!" />}
<div className="mb-6">
<PasswordInput
title="Confirm Password"
value={confirmPassword}
valid={!isButtonDisabled || (newPassword === confirmPassword && confirmPassword !== '')}
onChange={(e) => {
setConfirmPassword(e.target.value);
}}
/>
</div>
{newPassword === confirmPassword || confirmPassword === '' ? null : <ErrorBanner heading="Passwords do not match." description="Please make sure both passwords are the exact same!"/>}
<div className="flex flex-col items-left space-y-4">
<Button type="submit" disabled={isButtonDisabled} >
Send
</Button>
</div>
</>
);
}

View File

@ -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<HTMLInputElement>) => {
setEmail(event.currentTarget.value);
console.log("email " + email);
}
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.currentTarget.value);
console.log("password " + password)
}
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
// Priority: Incorrect combo > Missing email > Missing password
if (password.trim().length === 0) {
setError("Please enter your password.")
}
// This shouldn't happen, <input type="email"> 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 (
<>
<Paper>
<form className="mb-0 mt-6 mb-6 space-y-4 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white">
<form className="mb-0 m-auto mt-6 space-y-4 rounded-lg p-4 shadow-lg sm:p-6 lg:p-8 bg-white max-w-xl">
<Image
src="/logo.png"
alt='Compass Center logo.'
width={100}
height={91}
/>
<h1 className='font-bold text-xl text-purple-800'>Login</h1>
<div className="mb-4">
<Input type='email' title="Email" placeholder="janedoe@gmail.com" iconKey={'EmailInputIcon'} />
<Input type='email' title="Email" placeholder="janedoe@gmail.com" onChange={handleEmailChange} />
</div>
<div className="mb-6">
<Input type='password' title="Password" />
<Input type='password' title="Password" onChange={handlePasswordChange} />
</div>
<div className="flex flex-col items-left space-y-4">
<InlineLink>
<InlineLink href="/forgot_password">
Forgot password?
</InlineLink>
<Button>
<Button onClick={handleClick}>
Login
</Button>
<div className="text-center text-red-600" hidden={!error}>
<p>{error}</p>
</div>
</div>
</form>
@ -37,5 +83,4 @@ export default function Page() {
</Paper>
</>
);
};
};

View File

@ -2,22 +2,25 @@ import { FunctionComponent, ReactNode } from 'react';
type ButtonProps = {
children: ReactNode;
onClick?: () => void; // make the onClick handler optional
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit" | "reset"; // specify possible values for type
disabled?: boolean;
};
const Button: FunctionComponent<ButtonProps> = ({ children, onClick }) => {
const Button: FunctionComponent<ButtonProps> = ({ 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 (
<button
// className="px-4 py-2 font-bold text-white bg-purple-600 rounded hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-opacity-var focus:ring-color-var"
className="inline-block rounded border border-purple-600 bg-purple-600 px-12 py-3 text-sm font-semibold text-white hover:bg-transparent hover:text-purple-600 focus:outline-none focus:ring active:text-purple-500"
onClick={onClick}
// style={{
// '--ring-opacity-var': `var(--ring-opacity)`,
// '--ring-color-var': `rgba(var(--ring-color), var(--ring-opacity))`
// }}
className={buttonClassName}
onClick={onClick}
type={type}
disabled={disabled}
>
{children}
</button>
);
};
export default Button;

View File

@ -7,7 +7,7 @@ interface Link {
const InlineLink: React.FC<Link> = ({href = '#', children}) => {
return (
<a href={href} className='text-sm text-purple-600 hover:underline font-semibold italic'>
<a href={href} className='text-sm text-purple-600 hover:underline font-semibold'>
{children}
</a>
)

View File

@ -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<HTMLInputElement> & {
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<HTMLInputElement>) => void;
};
const Input: FunctionComponent<InputProps> = ({ iconKey, type, title, placeholder, ...rest }) => {
const IconComponent = iconKey ? Icons[iconKey] : null;
const Input: FunctionComponent<InputProps> = ({ icon, type, title, placeholder, onChange, valid = true, ...rest }) => {
return (
<div className="mb-4">
{title && (
<div className="mb-1">
<label htmlFor={title} className="text-sm font-semibold text-gray-700">
{title}
</label>
</div>
)}
<div className="flex items-center border border-gray-300 rounded-md shadow-sm overflow-hidden">
{IconComponent && (
<span className="inline-flex items-center px-3 border-r border-gray-300 text-gray-500">
<IconComponent className="h-5 w-5" />
</span>
)}
<input
{...rest}
type={type}
id={title}
placeholder={placeholder}
className="w-full border-none p-3 text-sm focus:ring-0"
style={{ boxShadow: 'none' }} // This ensures that the input doesn't have an inner shadow
/>
</div>
</div>
<div>
<label
htmlFor={title}
className={valid ? "block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-purple-600 focus-within:ring-1 focus-within:ring-purple-600" : "block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-red-600 focus-within:ring-1 focus-within:ring-red-600"}
>
<span className="text-xs font-semibold text-gray-700"> {title} </span>
<div className="mt-1 flex items-center">
<input
type={type}
id={title}
placeholder={placeholder}
onChange={onChange}
className="w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
<span className="inline-flex items-center px-3 text-gray-500">
{icon}
</span>
</div>
</label>
</div>
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
interface ErrorBannerProps {
heading: string;
description?: string | null;
}
const ErrorBanner: React.FC<ErrorBannerProps> = ({ heading, description = null }) => {
return (
<div role="alert" className="rounded border-s-4 border-red-500 bg-red-50 p-4">
<strong className="block text-sm font-semibold text-red-800">{heading}</strong>
{description && <p className="mt-2 text-xs font-thin text-red-700">
{description}
</p>}
</div>
);
};
export default ErrorBanner;

View File

@ -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<HTMLInputElement>) => void;
};
const PasswordInput: FunctionComponent<PasswordInputProps> = ({ 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 (
<Input
{...rest}
type={visible ? "text" : "password"}
onChange={onChange}
valid={valid}
icon={
<PasswordIcon className="h-5 w-5" onClick={toggleVisibility} />
}
/>
);
};
export default PasswordInput;

BIN
compass/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -8,7 +8,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",

View File

@ -0,0 +1,9 @@
import { InputHTMLAttributes } from "react";
import { Icons } from "../constants";
export type InputProps = InputHTMLAttributes<HTMLInputElement> & {
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;
};