Skip to content
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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
48 changes: 48 additions & 0 deletions libs/website/feature/contactpage/contactFormValidation.ts
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 */
Copy link
Collaborator

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

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)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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:

  1. Using a more robust email regex pattern
  2. Or better yet, using a validation library like validator.js for more reliable email validation
-  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)) {

Committable suggestion skipped: line range outside the PR's diff.

/* 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add consistent input sanitization and validation.

Several improvements needed for the validation logic:

  1. Add maximum length validation for all fields
  2. Extract magic numbers to named constants
  3. Add input sanitization to prevent XSS attacks
  4. Make trim() checks consistent across all validations
+ 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`
+  }

Committable suggestion skipped: line range outside the PR's diff.


return errors
}
62 changes: 62 additions & 0 deletions libs/website/feature/contactpage/index.tsx
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 */}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 */}
<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>
return (
<div
id="contactpage"
aria-label="Contact Us Page"
role="region"
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 */}
<ContactHero />
{/* Status Message */}
{submitStatus && (
<div
aria-live="polite"
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>

)
}
128 changes: 128 additions & 0 deletions libs/website/feature/contactpage/ui/ContactForm.tsx
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
Copy link

Choose a reason for hiding this comment

The 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:

  1. The timeout duration should be a constant
  2. Consider implementing rate limiting
  3. Add proper error handling for network failures
+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)
   }
 }

Committable suggestion skipped: line range outside the PR's diff.


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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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.

Committable suggestion skipped: line range outside the PR's diff.

}
17 changes: 17 additions & 0 deletions libs/website/feature/contactpage/ui/ContactHero.tsx
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">
<h1 className="text-3xl font-bold mb-3 pl-3 sm:pl-8">
Contact Us
</h1>
<p className="text-lg pl-5 sm:pl-10">
<TerminalText>
Feel free to ask us anything! We’re here to help.
</TerminalText>
</p>
</div>
)
}
29 changes: 29 additions & 0 deletions libs/website/feature/contactpage/ui/StatusMessage.tsx
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
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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:

  1. Move message strings to a constants file for better maintainability and future internationalization
  2. Add appropriate ARIA attributes for better accessibility
-      <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>

Committable suggestion skipped: line range outside the PR's diff.

</div>
)
}
3 changes: 3 additions & 0 deletions libs/website/pages/home.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react'
import { ContactPage } from '../feature/contactpage'
import { EventSection } from '../feature/events'
import { FAQSection } from '../feature/faq'
import { MissionSection, WelcomeSection } from '../feature/introduction'
import { SponsorshipSection } from '../feature/sponsorship'

import { Layout } from '../layouts/base'

export function Home() {
Expand All @@ -13,6 +15,7 @@ export function Home() {
<EventSection />
<SponsorshipSection />
<FAQSection />
<ContactPage />
</Layout>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const links = [
{ name: 'EVENTS', link: '/#events' },
{ name: 'SPONSORS', link: '/#sponsors' },
{ name: 'FAQ', link: '/#faq' },
{ name: 'CONTACT US', link: '/#contactpage' },
]

const socials = [
Expand Down
Loading