-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(website): create contact us page #199
base: main
Are you sure you want to change the base?
Changes from all commits
7ac2976
a2d8014
cf7039e
2aec2ac
b2d72fd
bb4368e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { FormData } from '../types/contact.types' | ||
|
||
export const DEFAULT_FORM_DATA: FormData = { | ||
name: '', | ||
email: '', | ||
subject: '', | ||
message: '', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export interface FormData { | ||
name: string | ||
email: string | ||
subject: string | ||
message: string | ||
} | ||
|
||
export interface FormErrors { | ||
name?: string | ||
email?: string | ||
subject?: string | ||
message?: string | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
export interface FormData { | ||
name: string | ||
email: string | ||
subject: string | ||
message: string | ||
} | ||
|
||
export interface FormErrors { | ||
name?: string | ||
email?: string | ||
subject?: string | ||
message?: string | ||
} | ||
|
||
export function validateForm(values: FormData): FormErrors { | ||
const errors: FormErrors = {} | ||
|
||
/* Name validation */ | ||
if (!values.name.trim()) { | ||
errors.name = 'Name is required' | ||
} | ||
else if (values.name.length < 2) { | ||
errors.name = 'Name must be at least 2 characters' | ||
} | ||
/* Email validation */ | ||
if (!values.email) { | ||
errors.email = 'Email is required' | ||
} | ||
else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice job on the regex, but we also need a solution that can check if the email itself is real. |
||
errors.email = 'Invalid email address' | ||
} | ||
Comment on lines
+29
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve email validation regex and consider using a validation library. The current email regex might miss some valid email patterns or accept invalid ones. Consider:
- else if (!/^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
+ else if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i.test(values.email)) {
|
||
/* Subject validation */ | ||
if (!values.subject.trim()) { | ||
errors.subject = 'Subject is required' | ||
} | ||
else if (values.subject.length < 3) { | ||
errors.subject = 'Subject must be at least 3 characters' | ||
} | ||
/* Message validation */ | ||
if (!values.message.trim()) { | ||
errors.message = 'Message is required' | ||
} | ||
else if (values.message.length < 10) { | ||
errors.message = 'Message must be at least 10 characters' | ||
} | ||
Comment on lines
+18
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add consistent input sanitization and validation. Several improvements needed for the validation logic:
+ const MAX_LENGTHS = {
+ name: 100,
+ subject: 200,
+ message: 1000
+ };
+
+ const MIN_LENGTHS = {
+ name: 2,
+ subject: 3,
+ message: 10
+ };
/* Name validation */
- if (!values.name.trim()) {
+ const sanitizedName = DOMPurify.sanitize(values.name.trim());
+ if (!sanitizedName) {
errors.name = 'Name is required'
}
- else if (values.name.length < 2) {
+ else if (sanitizedName.length < MIN_LENGTHS.name) {
errors.name = 'Name must be at least 2 characters'
}
+ else if (sanitizedName.length > MAX_LENGTHS.name) {
+ errors.name = `Name must not exceed ${MAX_LENGTHS.name} characters`
+ }
|
||
|
||
return errors | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import cuHackingLogo from '@cuhacking/shared/assets/logos/cuHacking/cuhacking-logo-1.svg' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { GlassmorphicCard } from '@cuhacking/shared/ui/src/cuHacking/components/glassmorphic-card' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import React, { useEffect, useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ContactForm } from './ui/ContactForm' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ContactHero } from './ui/ContactHero' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { StatusMessage } from './ui/StatusMessage' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export function ContactPage(): React.JSX.Element { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [submitStatus, setSubmitStatus] = useState<'success' | 'error' | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// hiding message after 3 seconds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (submitStatus) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const timer = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setSubmitStatus(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 3000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return () => clearTimeout(timer) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, [submitStatus]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const handleSubmit = (status: 'success' | 'error') => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setSubmitStatus(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setSubmitStatus(status) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 100) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
anakafeel marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div id="contactpage" className="flex justify-center w-full bg-black text-white min-h-screen"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div className="w-full max-w-screen-xl px-5 py-5 lg:px-20 lg:py-14"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<GlassmorphicCard className="flex flex-col md:flex-row gap-8 justify-between w-full h-auto p-8"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div className="flex flex-col gap-y-6 md:w-2/3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{/* Hero */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for comments here again. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<ContactHero /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{/* Status Message */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{submitStatus && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div className="transition-all duration-300 ease-in-out"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<StatusMessage type={submitStatus} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{/* Form */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<ContactForm | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onSubmit={handleSubmit} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{/* Logo */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<GlassmorphicCard className="flex items-center justify-center p-6 md:w-1/3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
src={cuHackingLogo} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
alt="cuHacking Logo" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
className="w-48 md:w-56 lg:w-64 transition-transform duration-300 hover:scale-110" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</GlassmorphicCard> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</GlassmorphicCard> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+29
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance accessibility with ARIA labels The contact page section and status message should have appropriate ARIA labels for better accessibility. - <div id="contactpage" className="flex justify-center w-full bg-black text-white min-h-screen">
+ <div
+ id="contactpage"
+ aria-label="Contact Us Page"
+ role="region"
+ className="flex justify-center w-full bg-black text-white min-h-screen"> Also, consider adding aria-live for the status message: - <div className="transition-all duration-300 ease-in-out">
+ <div
+ aria-live="polite"
+ className="transition-all duration-300 ease-in-out"> 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import type { FormData, FormErrors } from '../types/contact.types' | ||
import React, { useState } from 'react' | ||
import { validateForm } from '../contactFormValidation' | ||
|
||
interface ContactFormProps { | ||
onSubmit: (status: 'success' | 'error') => void | ||
} | ||
|
||
export function ContactForm({ onSubmit }: ContactFormProps): React.JSX.Element { | ||
const [formData, setFormData] = useState<FormData>({ | ||
name: '', | ||
email: '', | ||
subject: '', | ||
message: '', | ||
}) | ||
|
||
const [errors, setErrors] = useState<FormErrors>({}) | ||
const [isLoading, setIsLoading] = useState(false) | ||
|
||
const handleChange = ( | ||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, | ||
): void => { | ||
const { name, value } = e.target | ||
|
||
setFormData((prev: FormData) => ({ | ||
...prev, | ||
[name]: value, | ||
})) | ||
|
||
// Cleaning errors as user types | ||
if (errors[name as keyof FormErrors]) { | ||
setErrors((prev: FormErrors) => ({ | ||
...prev, | ||
[name]: undefined, | ||
})) | ||
} | ||
} | ||
|
||
const handleSubmit = async (e: React.FormEvent): Promise<void> => { | ||
e.preventDefault() | ||
|
||
const validationErrors = validateForm(formData) | ||
if (Object.keys(validationErrors).length > 0) { | ||
setErrors(validationErrors) | ||
return | ||
} | ||
|
||
setIsLoading(true) | ||
|
||
try { | ||
await new Promise<void>((resolve) => { | ||
setTimeout(resolve, 1500) | ||
}) | ||
onSubmit('success') | ||
setFormData({ name: '', email: '', subject: '', message: '' }) | ||
} | ||
catch (error) { | ||
console.error('Submission error:', error) | ||
onSubmit('error') | ||
} | ||
finally { | ||
setIsLoading(false) | ||
} | ||
} | ||
Comment on lines
+39
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance form submission handling Several improvements needed for the form submission:
+const SUBMISSION_TIMEOUT = 1500;
+const MAX_SUBMISSIONS = 3;
+const SUBMISSION_WINDOW = 60000; // 1 minute
+
+let submissionCount = 0;
+let lastSubmissionTime = 0;
+
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault()
+
+ const now = Date.now();
+ if (now - lastSubmissionTime > SUBMISSION_WINDOW) {
+ submissionCount = 0;
+ }
+
+ if (submissionCount >= MAX_SUBMISSIONS) {
+ onSubmit('error');
+ setErrors({ form: 'Too many attempts. Please try again later.' });
+ return;
+ }
+
+ submissionCount++;
+ lastSubmissionTime = now;
const validationErrors = validateForm(formData)
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors)
return
}
setIsLoading(true)
try {
await new Promise<void>((resolve) => {
- setTimeout(resolve, 1500)
+ setTimeout(resolve, SUBMISSION_TIMEOUT)
})
onSubmit('success')
setFormData({ name: '', email: '', subject: '', message: '' })
}
catch (error) {
console.error('Submission error:', error)
+ setErrors({ form: 'Failed to submit form. Please try again.' })
onSubmit('error')
}
finally {
setIsLoading(false)
}
}
|
||
|
||
return ( | ||
<div className="flex flex-col gap-y-6"> | ||
<form onSubmit={handleSubmit} className="flex flex-col gap-y-6"> | ||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6"> | ||
{/* Name */} | ||
<input | ||
type="text" | ||
name="name" | ||
value={formData.name} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.name ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
placeholder="Your Name" | ||
/> | ||
<input | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use shadcn's input box here for accessibility purposes |
||
type="email" | ||
name="email" | ||
value={formData.email} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.email ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
placeholder="Your Email" | ||
/> | ||
</div> | ||
|
||
{/* Subject */} | ||
<input | ||
type="text" | ||
name="subject" | ||
value={formData.subject} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.subject ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
placeholder="Subject" | ||
/> | ||
|
||
{/* Message */} | ||
<textarea | ||
name="message" | ||
value={formData.message} | ||
onChange={handleChange} | ||
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${ | ||
errors.message ? 'ring-2 ring-red-500' : 'focus:ring-green-500' | ||
}`} | ||
rows={5} | ||
placeholder="Your Message" | ||
/> | ||
|
||
{/* Submit Button */} | ||
<button | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be a shadcn button |
||
type="submit" | ||
className="py-3 px-8 bg-green-500 text-black rounded-lg font-semibold hover:bg-green-600 w-full sm:w-auto" | ||
disabled={isLoading} | ||
> | ||
{isLoading ? 'Sending...' : 'Send Message'} | ||
</button> | ||
</form> | ||
</div> | ||
) | ||
Comment on lines
+66
to
+127
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve form accessibility and user feedback The form needs proper labels and error messages for better accessibility and user experience. <div className="flex flex-col gap-y-6">
<form onSubmit={handleSubmit} className="flex flex-col gap-y-6">
+ {errors.form && (
+ <div role="alert" className="text-red-500">
+ {errors.form}
+ </div>
+ )}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Name */}
+ <div className="flex flex-col gap-y-1">
+ <label htmlFor="name" className="sr-only">Name</label>
<input
+ id="name"
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={`p-3 bg-gray-900 rounded-lg text-white focus:outline-none focus:ring-2 ${
errors.name ? 'ring-2 ring-red-500' : 'focus:ring-green-500'
}`}
placeholder="Your Name"
+ aria-invalid={errors.name ? 'true' : 'false'}
/>
+ {errors.name && (
+ <span className="text-red-500 text-sm" role="alert">
+ {errors.name}
+ </span>
+ )}
+ </div> Apply similar changes to email, subject, and message fields.
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { TerminalText } from '@cuhacking/shared/ui/src/cuHacking/components/terminal-text' | ||
import React from 'react' | ||
|
||
export function ContactHero(): React.JSX.Element { | ||
return ( | ||
<div className="mb-8"> | ||
<h2 className="text-3xl font-bold mb-3 pl-3 sm:pl-8"> | ||
Contact Us | ||
</h2> | ||
<p className="text-lg pl-5 sm:pl-10"> | ||
<TerminalText> | ||
Feel free to ask us anything! We’re here to help. | ||
</TerminalText> | ||
</p> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React from 'react' | ||
|
||
interface StatusMessageProps { | ||
type: 'success' | 'error' | ||
} | ||
|
||
export function StatusMessage({ type }: StatusMessageProps): React.JSX.Element { | ||
return ( | ||
<div | ||
className={`p-4 rounded-lg flex items-center gap-2 ${ | ||
type === 'success' | ||
? 'bg-green-500/20 border border-green-500/50 text-green-400' | ||
: 'bg-red-500/20 border border-red-500/50 text-red-400' | ||
}`} | ||
> | ||
{/* Icon */} | ||
<span className="text-xl"> | ||
{type === 'success' ? '✅' : '❌'} | ||
</span> | ||
Comment on lines
+17
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Replace emojis with consistent SVG icons. Using emojis for status icons may lead to inconsistent rendering across different platforms and browsers. Consider using SVG icons from a UI library for better consistency and customization. |
||
|
||
{/* Message */} | ||
<p className="font-medium"> | ||
{type === 'success' | ||
? 'Message sent successfully!' | ||
: 'Failed to send message. Please try again.'} | ||
</p> | ||
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Extract status messages to constants and add accessibility attributes. Consider the following improvements:
- <p className="font-medium">
+ <p
+ className="font-medium"
+ role="status"
+ aria-live="polite"
+ >
{type === 'success'
- ? 'Message sent successfully!'
- : 'Failed to send message. Please try again.'}
+ ? STATUS_MESSAGES.success
+ : STATUS_MESSAGES.error}
</p>
|
||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import playwright from 'eslint-plugin-playwright' | ||
import baseConfigPromise from '../../eslint.config.js' | ||
|
||
export default (async () => { | ||
const baseConfig = await baseConfigPromise | ||
|
||
return [ | ||
playwright.configs['flat/recommended'], | ||
...baseConfig, | ||
{ | ||
files: ['**/*.ts', '**/*.js'], | ||
// Override or add rules here | ||
rules: {}, | ||
}, | ||
] | ||
})() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"name": "risk-form-filler", | ||
"type": "module", | ||
"description": "", | ||
"license": "", | ||
"sideEffects": false, | ||
"scripts": {} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need these comments here, the code is fairly self explanatory