feat(careers): added a careers page (#1746)

* feat(careers): added a careers page

* cleanup

* revert hardcoded environment
This commit is contained in:
Waleed
2025-10-27 20:25:44 -07:00
committed by GitHub
parent 8620ab255a
commit 095a15d7b5
9 changed files with 1208 additions and 12 deletions

View File

@@ -0,0 +1,524 @@
'use client'
import { useRef, useState } from 'react'
import { Loader2, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { quickValidateEmail } from '@/lib/email/validation'
import { cn } from '@/lib/utils'
import { LegalLayout } from '@/app/(landing)/components'
import { soehne } from '@/app/fonts/soehne/soehne'
const validateName = (name: string): string[] => {
const errors: string[] = []
if (!name || name.trim().length < 2) {
errors.push('Name must be at least 2 characters')
}
return errors
}
const validateEmail = (email: string): string[] => {
const errors: string[] = []
if (!email || !email.trim()) {
errors.push('Email is required')
return errors
}
const validation = quickValidateEmail(email.trim().toLowerCase())
if (!validation.isValid) {
errors.push(validation.reason || 'Please enter a valid email address')
}
return errors
}
const validatePosition = (position: string): string[] => {
const errors: string[] = []
if (!position || position.trim().length < 2) {
errors.push('Please specify the position you are interested in')
}
return errors
}
const validateLinkedIn = (url: string): string[] => {
if (!url || url.trim() === '') return []
const errors: string[] = []
try {
new URL(url)
} catch {
errors.push('Please enter a valid LinkedIn URL')
}
return errors
}
const validatePortfolio = (url: string): string[] => {
if (!url || url.trim() === '') return []
const errors: string[] = []
try {
new URL(url)
} catch {
errors.push('Please enter a valid portfolio URL')
}
return errors
}
const validateLocation = (location: string): string[] => {
const errors: string[] = []
if (!location || location.trim().length < 2) {
errors.push('Please enter your location')
}
return errors
}
const validateMessage = (message: string): string[] => {
const errors: string[] = []
if (!message || message.trim().length < 50) {
errors.push('Please tell us more about yourself (at least 50 characters)')
}
return errors
}
export default function CareersPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle')
const [showErrors, setShowErrors] = useState(false)
// Form fields
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [position, setPosition] = useState('')
const [linkedin, setLinkedin] = useState('')
const [portfolio, setPortfolio] = useState('')
const [experience, setExperience] = useState('')
const [location, setLocation] = useState('')
const [message, setMessage] = useState('')
const [resume, setResume] = useState<File | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// Field errors
const [nameErrors, setNameErrors] = useState<string[]>([])
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [positionErrors, setPositionErrors] = useState<string[]>([])
const [linkedinErrors, setLinkedinErrors] = useState<string[]>([])
const [portfolioErrors, setPortfolioErrors] = useState<string[]>([])
const [experienceErrors, setExperienceErrors] = useState<string[]>([])
const [locationErrors, setLocationErrors] = useState<string[]>([])
const [messageErrors, setMessageErrors] = useState<string[]>([])
const [resumeErrors, setResumeErrors] = useState<string[]>([])
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null
setResume(file)
if (file) {
setResumeErrors([])
}
}
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setShowErrors(true)
// Validate all fields
const nameErrs = validateName(name)
const emailErrs = validateEmail(email)
const positionErrs = validatePosition(position)
const linkedinErrs = validateLinkedIn(linkedin)
const portfolioErrs = validatePortfolio(portfolio)
const experienceErrs = experience ? [] : ['Please select your years of experience']
const locationErrs = validateLocation(location)
const messageErrs = validateMessage(message)
const resumeErrs = resume ? [] : ['Resume is required']
setNameErrors(nameErrs)
setEmailErrors(emailErrs)
setPositionErrors(positionErrs)
setLinkedinErrors(linkedinErrs)
setPortfolioErrors(portfolioErrs)
setExperienceErrors(experienceErrs)
setLocationErrors(locationErrs)
setMessageErrors(messageErrs)
setResumeErrors(resumeErrs)
if (
nameErrs.length > 0 ||
emailErrs.length > 0 ||
positionErrs.length > 0 ||
linkedinErrs.length > 0 ||
portfolioErrs.length > 0 ||
experienceErrs.length > 0 ||
locationErrs.length > 0 ||
messageErrs.length > 0 ||
resumeErrs.length > 0
) {
return
}
setIsSubmitting(true)
setSubmitStatus('idle')
try {
const formData = new FormData()
formData.append('name', name)
formData.append('email', email)
formData.append('phone', phone || '')
formData.append('position', position)
formData.append('linkedin', linkedin || '')
formData.append('portfolio', portfolio || '')
formData.append('experience', experience)
formData.append('location', location)
formData.append('message', message)
if (resume) formData.append('resume', resume)
const response = await fetch('/api/careers/submit', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error('Failed to submit application')
}
setSubmitStatus('success')
} catch (error) {
console.error('Error submitting application:', error)
setSubmitStatus('error')
} finally {
setIsSubmitting(false)
}
}
return (
<LegalLayout title='Join Our Team'>
<div className={`${soehne.className} mx-auto max-w-2xl`}>
{/* Form Section */}
<section className='rounded-2xl border border-gray-200 bg-white p-6 shadow-sm sm:p-10'>
<h2 className='mb-2 font-medium text-2xl sm:text-3xl'>Apply Now</h2>
<p className='mb-8 text-gray-600 text-sm sm:text-base'>
Help us build the future of AI workflows
</p>
<form onSubmit={onSubmit} className='space-y-5'>
{/* Name and Email */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='name' className='font-medium text-sm'>
Full Name *
</Label>
<Input
id='name'
placeholder='John Doe'
value={name}
onChange={(e) => setName(e.target.value)}
className={cn(
showErrors &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && nameErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='email' className='font-medium text-sm'>
Email *
</Label>
<Input
id='email'
type='email'
placeholder='john@example.com'
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
showErrors &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Phone and Position */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='phone' className='font-medium text-sm'>
Phone Number
</Label>
<Input
id='phone'
type='tel'
placeholder='+1 (555) 123-4567'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='position' className='font-medium text-sm'>
Position of Interest *
</Label>
<Input
id='position'
placeholder='e.g. Full Stack Engineer, Product Designer'
value={position}
onChange={(e) => setPosition(e.target.value)}
className={cn(
showErrors &&
positionErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && positionErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{positionErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* LinkedIn and Portfolio */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='linkedin' className='font-medium text-sm'>
LinkedIn Profile
</Label>
<Input
id='linkedin'
placeholder='https://linkedin.com/in/yourprofile'
value={linkedin}
onChange={(e) => setLinkedin(e.target.value)}
className={cn(
showErrors &&
linkedinErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && linkedinErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{linkedinErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='portfolio' className='font-medium text-sm'>
Portfolio / Website
</Label>
<Input
id='portfolio'
placeholder='https://yourportfolio.com'
value={portfolio}
onChange={(e) => setPortfolio(e.target.value)}
className={cn(
showErrors &&
portfolioErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && portfolioErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{portfolioErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Experience and Location */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='experience' className='font-medium text-sm'>
Years of Experience *
</Label>
<Select value={experience} onValueChange={setExperience}>
<SelectTrigger
className={cn(
showErrors &&
experienceErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
>
<SelectValue placeholder='Select experience level' />
</SelectTrigger>
<SelectContent>
<SelectItem value='0-1'>0-1 years</SelectItem>
<SelectItem value='1-3'>1-3 years</SelectItem>
<SelectItem value='3-5'>3-5 years</SelectItem>
<SelectItem value='5-10'>5-10 years</SelectItem>
<SelectItem value='10+'>10+ years</SelectItem>
</SelectContent>
</Select>
{showErrors && experienceErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{experienceErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='location' className='font-medium text-sm'>
Location *
</Label>
<Input
id='location'
placeholder='e.g. San Francisco, CA'
value={location}
onChange={(e) => setLocation(e.target.value)}
className={cn(
showErrors &&
locationErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && locationErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{locationErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Message */}
<div className='space-y-2'>
<Label htmlFor='message' className='font-medium text-sm'>
Tell us about yourself *
</Label>
<Textarea
id='message'
placeholder='Tell us about your experience, what excites you about Sim, and why you would be a great fit for this role...'
className={cn(
'min-h-[140px]',
showErrors &&
messageErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<p className='mt-1.5 text-gray-500 text-xs'>Minimum 50 characters</p>
{showErrors && messageErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{messageErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{/* Resume Upload */}
<div className='space-y-2'>
<Label htmlFor='resume' className='font-medium text-sm'>
Resume *
</Label>
<div className='relative'>
{resume ? (
<div className='flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2'>
<span className='flex-1 truncate text-sm'>{resume.name}</span>
<button
type='button'
onClick={(e) => {
e.preventDefault()
setResume(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}}
className='flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground'
aria-label='Remove file'
>
<X className='h-4 w-4' />
</button>
</div>
) : (
<Input
id='resume'
type='file'
accept='.pdf,.doc,.docx'
onChange={handleFileChange}
ref={fileInputRef}
className={cn(
showErrors &&
resumeErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
)}
</div>
<p className='mt-1.5 text-gray-500 text-xs'>PDF or Word document, max 10MB</p>
{showErrors && resumeErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{resumeErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{/* Submit Button */}
<div className='flex justify-end pt-2'>
<Button
type='submit'
disabled={isSubmitting || submitStatus === 'success'}
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
size='lg'
>
{isSubmitting ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Submitting...
</>
) : submitStatus === 'success' ? (
'Submitted'
) : (
'Submit Application'
)}
</Button>
</div>
</form>
</section>
{/* Additional Info */}
<section className='mt-6 text-center text-gray-600 text-sm'>
<p>
Questions? Email us at{' '}
<a
href='mailto:careers@sim.ai'
className='font-medium text-gray-900 underline transition-colors hover:text-gray-700'
>
careers@sim.ai
</a>
</p>
</section>
</div>
</LegalLayout>
)
}

View File

@@ -228,6 +228,12 @@ export default function Footer({ fullWidth = false }: FooterProps) {
>
Changelog
</Link>
<Link
href='/careers'
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
>
Careers
</Link>
<Link
href='/privacy'
target='_blank'

View File

@@ -20,7 +20,7 @@ interface NavProps {
}
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
const [githubStars, setGithubStars] = useState('16.3k')
const [githubStars, setGithubStars] = useState('17.4k')
const [isHovered, setIsHovered] = useState(false)
const [isLoginHovered, setIsLoginHovered] = useState(false)
const router = useRouter()
@@ -113,7 +113,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
itemType='https://schema.org/SiteNavigationElement'
>
<div className='flex items-center gap-[34px]'>
<Link href='/' aria-label={`${brand.name} home`} itemProp='url'>
<Link href='/?from=nav' aria-label={`${brand.name} home`} itemProp='url'>
<span itemProp='name' className='sr-only'>
{brand.name} Home
</span>

View File

@@ -0,0 +1,200 @@
import { render } from '@react-email/components'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import CareersConfirmationEmail from '@/components/emails/careers-confirmation-email'
import CareersSubmissionEmail from '@/components/emails/careers-submission-email'
import { sendEmail } from '@/lib/email/mailer'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('CareersAPI')
// Max file size: 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024
const ALLOWED_FILE_TYPES = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]
const CareersSubmissionSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email address'),
phone: z.string().optional(),
position: z.string().min(2, 'Please specify the position you are interested in'),
linkedin: z.string().url('Please enter a valid LinkedIn URL').optional().or(z.literal('')),
portfolio: z.string().url('Please enter a valid portfolio URL').optional().or(z.literal('')),
experience: z.enum(['0-1', '1-3', '3-5', '5-10', '10+']),
location: z.string().min(2, 'Please enter your location'),
message: z.string().min(50, 'Please tell us more about yourself (at least 50 characters)'),
})
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const formData = await request.formData()
// Extract form fields
const data = {
name: formData.get('name') as string,
email: formData.get('email') as string,
phone: formData.get('phone') as string,
position: formData.get('position') as string,
linkedin: formData.get('linkedin') as string,
portfolio: formData.get('portfolio') as string,
experience: formData.get('experience') as string,
location: formData.get('location') as string,
message: formData.get('message') as string,
}
// Extract and validate resume file
const resumeFile = formData.get('resume') as File | null
if (!resumeFile) {
return NextResponse.json(
{
success: false,
message: 'Resume is required',
errors: [{ path: ['resume'], message: 'Resume is required' }],
},
{ status: 400 }
)
}
// Validate file size
if (resumeFile.size > MAX_FILE_SIZE) {
return NextResponse.json(
{
success: false,
message: 'Resume file size must be less than 10MB',
errors: [{ path: ['resume'], message: 'File size must be less than 10MB' }],
},
{ status: 400 }
)
}
// Validate file type
if (!ALLOWED_FILE_TYPES.includes(resumeFile.type)) {
return NextResponse.json(
{
success: false,
message: 'Resume must be a PDF or Word document',
errors: [{ path: ['resume'], message: 'File must be PDF or Word document' }],
},
{ status: 400 }
)
}
// Convert file to base64 for email attachment
const resumeBuffer = await resumeFile.arrayBuffer()
const resumeBase64 = Buffer.from(resumeBuffer).toString('base64')
const validatedData = CareersSubmissionSchema.parse(data)
logger.info(`[${requestId}] Processing career application`, {
name: validatedData.name,
email: validatedData.email,
position: validatedData.position,
resumeSize: resumeFile.size,
resumeType: resumeFile.type,
})
const submittedDate = new Date()
const careersEmailHtml = await render(
CareersSubmissionEmail({
name: validatedData.name,
email: validatedData.email,
phone: validatedData.phone,
position: validatedData.position,
linkedin: validatedData.linkedin,
portfolio: validatedData.portfolio,
experience: validatedData.experience,
location: validatedData.location,
message: validatedData.message,
submittedDate,
})
)
const confirmationEmailHtml = await render(
CareersConfirmationEmail({
name: validatedData.name,
position: validatedData.position,
submittedDate,
})
)
// Send email with resume attachment
const careersEmailResult = await sendEmail({
to: 'careers@sim.ai',
subject: `New Career Application: ${validatedData.name} - ${validatedData.position}`,
html: careersEmailHtml,
emailType: 'transactional',
replyTo: validatedData.email,
attachments: [
{
filename: resumeFile.name,
content: resumeBase64,
contentType: resumeFile.type,
},
],
})
if (!careersEmailResult.success) {
logger.error(`[${requestId}] Failed to send email to careers@sim.ai`, {
error: careersEmailResult.message,
})
throw new Error('Failed to submit application')
}
const confirmationResult = await sendEmail({
to: validatedData.email,
subject: `Your Application to Sim - ${validatedData.position}`,
html: confirmationEmailHtml,
emailType: 'transactional',
replyTo: validatedData.email,
})
if (!confirmationResult.success) {
logger.warn(`[${requestId}] Failed to send confirmation email to applicant`, {
email: validatedData.email,
error: confirmationResult.message,
})
}
logger.info(`[${requestId}] Career application submitted successfully`, {
careersEmailSent: careersEmailResult.success,
confirmationEmailSent: confirmationResult.success,
})
return NextResponse.json({
success: true,
message: 'Application submitted successfully',
})
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid application data`, { errors: error.errors })
return NextResponse.json(
{
success: false,
message: 'Invalid application data',
errors: error.errors,
},
{ status: 400 }
)
}
logger.error(`[${requestId}] Error processing career application:`, error)
return NextResponse.json(
{
success: false,
message:
'Failed to submit application. Please try again or email us directly at careers@sim.ai',
},
{ status: 500 }
)
}
}

View File

@@ -10,7 +10,6 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
// Force light mode for certain pages
const forcedTheme =
pathname === '/' ||
pathname === '/homepage' ||
pathname.startsWith('/login') ||
pathname.startsWith('/signup') ||
pathname.startsWith('/sso') ||
@@ -18,6 +17,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
pathname.startsWith('/privacy') ||
pathname.startsWith('/invite') ||
pathname.startsWith('/verify') ||
pathname.startsWith('/careers') ||
pathname.startsWith('/changelog') ||
pathname.startsWith('/chat') ||
pathname.startsWith('/blog')

View File

@@ -198,7 +198,7 @@ export function SettingsNavigation({
})
const handleHomepageClick = () => {
window.location.href = '/homepage'
window.location.href = '/?from=settings'
}
return (

View File

@@ -0,0 +1,153 @@
import {
Body,
Column,
Container,
Head,
Html,
Img,
Preview,
Row,
Section,
Text,
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
interface CareersConfirmationEmailProps {
name: string
position: string
submittedDate?: Date
}
export const CareersConfirmationEmail = ({
name,
position,
submittedDate = new Date(),
}: CareersConfirmationEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
return (
<Html>
<Head />
<Body style={baseStyles.main}>
<Preview>Your application to {brand.name} has been received</Preview>
<Container style={baseStyles.container}>
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={brand.logoUrl || `${baseUrl}/logo/reverse/text/medium.png`}
width='114'
alt={brand.name}
style={{
margin: '0 auto',
}}
/>
</Column>
</Row>
</Section>
<Section style={baseStyles.sectionsBorders}>
<Row>
<Column style={baseStyles.sectionBorder} />
<Column style={baseStyles.sectionCenter} />
<Column style={baseStyles.sectionBorder} />
</Row>
</Section>
<Section style={baseStyles.content}>
<Text style={baseStyles.paragraph}>Hello {name},</Text>
<Text style={baseStyles.paragraph}>
Thank you for your interest in joining the {brand.name} team! We've received your
application for the <strong>{position}</strong> position.
</Text>
<Text style={baseStyles.paragraph}>
Our team carefully reviews every application and will get back to you within the next
few weeks. If your qualifications match what we're looking for, we'll reach out to
schedule an initial conversation.
</Text>
<Section
style={{
marginTop: '24px',
marginBottom: '24px',
padding: '20px',
backgroundColor: '#f9f9f9',
borderRadius: '8px',
border: '1px solid #e5e5e5',
}}
>
<Text
style={{
margin: '0 0 12px 0',
fontSize: '16px',
fontWeight: 'bold',
color: '#333333',
}}
>
What Happens Next?
</Text>
<ul
style={{
margin: '0',
padding: '0 0 0 20px',
fontSize: '14px',
color: '#333333',
lineHeight: '1.8',
}}
>
<li>Our team will review your application</li>
<li>If you're a good fit, we'll reach out to schedule an interview</li>
<li>We'll keep you updated throughout the process</li>
</ul>
</Section>
<Text style={baseStyles.paragraph}>
In the meantime, feel free to explore our{' '}
<a
href='https://docs.sim.ai'
target='_blank'
rel='noopener noreferrer'
style={{ color: '#802FFF', textDecoration: 'none' }}
>
documentation
</a>{' '}
to learn more about what we're building, or check out our{' '}
<a href={`${baseUrl}/blog`} style={{ color: '#802FFF', textDecoration: 'none' }}>
blog
</a>{' '}
for the latest updates.
</Text>
<Text style={baseStyles.paragraph}>
Best regards,
<br />
The {brand.name} Team
</Text>
<Text
style={{
...baseStyles.footerText,
marginTop: '40px',
textAlign: 'left',
color: '#666666',
}}
>
This confirmation was sent on {format(submittedDate, 'MMMM do, yyyy')} at{' '}
{format(submittedDate, 'h:mm a')}.
</Text>
</Section>
</Container>
<EmailFooter baseUrl={baseUrl} />
</Body>
</Html>
)
}
export default CareersConfirmationEmail

View File

@@ -0,0 +1,313 @@
import {
Body,
Column,
Container,
Head,
Html,
Img,
Preview,
Row,
Section,
Text,
} from '@react-email/components'
import { format } from 'date-fns'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/urls/utils'
import { baseStyles } from './base-styles'
interface CareersSubmissionEmailProps {
name: string
email: string
phone?: string
position: string
linkedin?: string
portfolio?: string
experience: string
location: string
message: string
submittedDate?: Date
}
const getExperienceLabel = (experience: string) => {
const labels: Record<string, string> = {
'0-1': '0-1 years',
'1-3': '1-3 years',
'3-5': '3-5 years',
'5-10': '5-10 years',
'10+': '10+ years',
}
return labels[experience] || experience
}
export const CareersSubmissionEmail = ({
name,
email,
phone,
position,
linkedin,
portfolio,
experience,
location,
message,
submittedDate = new Date(),
}: CareersSubmissionEmailProps) => {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
return (
<Html>
<Head />
<Body style={baseStyles.main}>
<Preview>New Career Application from {name}</Preview>
<Container style={baseStyles.container}>
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={brand.logoUrl || `${baseUrl}/logo/reverse/text/medium.png`}
width='114'
alt={brand.name}
style={{
margin: '0 auto',
}}
/>
</Column>
</Row>
</Section>
<Section style={baseStyles.sectionsBorders}>
<Row>
<Column style={baseStyles.sectionBorder} />
<Column style={baseStyles.sectionCenter} />
<Column style={baseStyles.sectionBorder} />
</Row>
</Section>
<Section style={baseStyles.content}>
<Text style={{ ...baseStyles.paragraph, fontSize: '18px', fontWeight: 'bold' }}>
New Career Application
</Text>
<Text style={baseStyles.paragraph}>
A new career application has been submitted on{' '}
{format(submittedDate, 'MMMM do, yyyy')} at {format(submittedDate, 'h:mm a')}.
</Text>
{/* Applicant Information */}
<Section
style={{
marginTop: '24px',
marginBottom: '24px',
padding: '20px',
backgroundColor: '#f9f9f9',
borderRadius: '8px',
border: '1px solid #e5e5e5',
}}
>
<Text
style={{
margin: '0 0 16px 0',
fontSize: '16px',
fontWeight: 'bold',
color: '#333333',
}}
>
Applicant Information
</Text>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
width: '40%',
}}
>
Name:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>{name}</td>
</tr>
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
Email:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
<a
href={`mailto:${email}`}
style={{ color: '#802FFF', textDecoration: 'none' }}
>
{email}
</a>
</td>
</tr>
{phone && (
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
Phone:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
<a href={`tel:${phone}`} style={{ color: '#802FFF', textDecoration: 'none' }}>
{phone}
</a>
</td>
</tr>
)}
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
Position:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
{position}
</td>
</tr>
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
Experience:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
{getExperienceLabel(experience)}
</td>
</tr>
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
Location:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
{location}
</td>
</tr>
{linkedin && (
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
LinkedIn:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
<a
href={linkedin}
target='_blank'
rel='noopener noreferrer'
style={{ color: '#802FFF', textDecoration: 'none' }}
>
View Profile
</a>
</td>
</tr>
)}
{portfolio && (
<tr>
<td
style={{
padding: '8px 0',
fontSize: '14px',
fontWeight: 'bold',
color: '#666666',
}}
>
Portfolio:
</td>
<td style={{ padding: '8px 0', fontSize: '14px', color: '#333333' }}>
<a
href={portfolio}
target='_blank'
rel='noopener noreferrer'
style={{ color: '#802FFF', textDecoration: 'none' }}
>
View Portfolio
</a>
</td>
</tr>
)}
</table>
</Section>
{/* Message */}
<Section
style={{
marginTop: '24px',
marginBottom: '24px',
padding: '20px',
backgroundColor: '#f9f9f9',
borderRadius: '8px',
border: '1px solid #e5e5e5',
}}
>
<Text
style={{
margin: '0 0 12px 0',
fontSize: '16px',
fontWeight: 'bold',
color: '#333333',
}}
>
About Themselves
</Text>
<Text
style={{
margin: '0',
fontSize: '14px',
color: '#333333',
lineHeight: '1.6',
whiteSpace: 'pre-wrap',
}}
>
{message}
</Text>
</Section>
<Text style={baseStyles.paragraph}>
Please review this application and reach out to the candidate at your earliest
convenience.
</Text>
</Section>
</Container>
</Body>
</Html>
)
}
export default CareersSubmissionEmail

View File

@@ -23,7 +23,7 @@ function handleRootPathRedirects(
): NextResponse | null {
const url = request.nextUrl
if (url.pathname !== '/' && url.pathname !== '/homepage') {
if (url.pathname !== '/') {
return null
}
@@ -35,14 +35,14 @@ function handleRootPathRedirects(
return NextResponse.redirect(new URL('/login', request.url))
}
// Hosted: Allow access to /homepage route even for authenticated users
if (url.pathname === '/homepage') {
return NextResponse.rewrite(new URL('/', request.url))
}
// For root path, redirect authenticated users to workspace
if (hasActiveSession && url.pathname === '/') {
return NextResponse.redirect(new URL('/workspace', request.url))
// Unless they have a 'from' query parameter (e.g., ?from=nav, ?from=settings)
// This allows intentional navigation to the homepage from anywhere in the app
if (hasActiveSession) {
const from = url.searchParams.get('from')
if (!from) {
return NextResponse.redirect(new URL('/workspace', request.url))
}
}
return null