mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
chore(careers): remove careers page, redirect to Ashby jobs portal (#3401)
* chore(careers): remove careers page, redirect to Ashby jobs portal * lint
This commit is contained in:
@@ -1,534 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { X } from 'lucide-react'
|
||||
import { Textarea } from '@/components/emcn'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
|
||||
const logger = createLogger('CareersPage')
|
||||
|
||||
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) {
|
||||
logger.error('Error submitting application:', error)
|
||||
setSubmitStatus('error')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`${soehne.className} min-h-screen bg-white text-gray-900`}>
|
||||
<Nav variant='landing' />
|
||||
|
||||
{/* Content */}
|
||||
<div className='px-4 pt-[60px] pb-[80px] sm:px-8 md:px-[44px]'>
|
||||
<h1 className='mb-10 text-center font-bold text-4xl text-gray-900 md:text-5xl'>
|
||||
Join Our Team
|
||||
</h1>
|
||||
|
||||
<div className='mx-auto max-w-4xl'>
|
||||
{/* Form Section */}
|
||||
<section className='rounded-2xl border border-gray-200 bg-white p-6 shadow-sm sm:p-10'>
|
||||
<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'>
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmitting || submitStatus === 'success'}
|
||||
loading={isSubmitting}
|
||||
loadingText='Submitting'
|
||||
showArrow={false}
|
||||
fullWidth={false}
|
||||
className='min-w-[200px]'
|
||||
>
|
||||
{submitStatus === 'success' ? 'Submitted' : 'Submit Application'}
|
||||
</BrandedButton>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* Footer - Only for hosted instances */}
|
||||
{isHosted && (
|
||||
<div className='relative z-20'>
|
||||
<Footer fullWidth={true} />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -77,12 +77,14 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
<Link
|
||||
href='/careers'
|
||||
<a
|
||||
href='https://jobs.ashbyhq.com/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Careers
|
||||
</Link>
|
||||
</a>
|
||||
<Link
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
|
||||
@@ -91,12 +91,14 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href='/careers'
|
||||
<a
|
||||
href='https://jobs.ashbyhq.com/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Careers
|
||||
</Link>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
||||
@@ -18,7 +18,6 @@ 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('/studio') ||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { render } from '@react-email/components'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { CareersConfirmationEmail, CareersSubmissionEmail } from '@/components/emails'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('CareersAPI')
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
)
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
renderBatchInvitationEmail,
|
||||
renderCareersConfirmationEmail,
|
||||
renderCareersSubmissionEmail,
|
||||
renderCreditPurchaseEmail,
|
||||
renderEnterpriseSubscriptionEmail,
|
||||
renderFreeTierUpgradeEmail,
|
||||
@@ -94,22 +92,6 @@ const emailTemplates = {
|
||||
failureReason: 'Card declined',
|
||||
}),
|
||||
|
||||
// Careers emails
|
||||
'careers-confirmation': () => renderCareersConfirmationEmail('John Doe', 'Senior Engineer'),
|
||||
'careers-submission': () =>
|
||||
renderCareersSubmissionEmail({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '+1 (555) 123-4567',
|
||||
position: 'Senior Engineer',
|
||||
linkedin: 'https://linkedin.com/in/johndoe',
|
||||
portfolio: 'https://johndoe.dev',
|
||||
experience: '5-10',
|
||||
location: 'San Francisco, CA',
|
||||
message:
|
||||
'I have 10 years of experience building scalable distributed systems. Most recently, I led a team at a Series B startup where we scaled from 100K to 10M users.',
|
||||
}),
|
||||
|
||||
// Notification emails
|
||||
'workflow-notification-success': () =>
|
||||
renderWorkflowNotificationEmail({
|
||||
@@ -176,7 +158,6 @@ export async function GET(request: NextRequest) {
|
||||
'credit-purchase',
|
||||
'payment-failed',
|
||||
],
|
||||
Careers: ['careers-confirmation', 'careers-submission'],
|
||||
Notifications: [
|
||||
'workflow-notification-success',
|
||||
'workflow-notification-error',
|
||||
|
||||
@@ -55,7 +55,7 @@ Sim provides a visual drag-and-drop interface for building and deploying AI agen
|
||||
|
||||
## Optional
|
||||
|
||||
- [Careers](${baseUrl}/careers): Join the Sim team
|
||||
- [Careers](https://jobs.ashbyhq.com/sim): Join the Sim team
|
||||
- [Terms of Service](${baseUrl}/terms): Legal terms
|
||||
- [Privacy Policy](${baseUrl}/privacy): Data handling practices
|
||||
`
|
||||
|
||||
@@ -28,10 +28,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
url: `${baseUrl}/changelog`,
|
||||
lastModified: now,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/careers`,
|
||||
lastModified: new Date('2024-10-06'),
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/terms`,
|
||||
lastModified: new Date('2024-10-14'),
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Text } from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface CareersConfirmationEmailProps {
|
||||
name: string
|
||||
position: string
|
||||
submittedDate?: Date
|
||||
}
|
||||
|
||||
export function CareersConfirmationEmail({
|
||||
name,
|
||||
position,
|
||||
submittedDate = new Date(),
|
||||
}: CareersConfirmationEmailProps) {
|
||||
const brand = getBrandConfig()
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
return (
|
||||
<EmailLayout
|
||||
preview={`Your application to ${brand.name} has been received`}
|
||||
showUnsubscribe={false}
|
||||
>
|
||||
<Text style={baseStyles.paragraph}>Hello {name},</Text>
|
||||
<Text style={baseStyles.paragraph}>
|
||||
We've received your application for <strong>{position}</strong>. Our team reviews every
|
||||
application and will reach out if there's a match.
|
||||
</Text>
|
||||
|
||||
<Text style={baseStyles.paragraph}>
|
||||
In the meantime, explore our{' '}
|
||||
<a
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
style={baseStyles.link}
|
||||
>
|
||||
docs
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href={`${baseUrl}/studio`} style={baseStyles.link}>
|
||||
blog
|
||||
</a>{' '}
|
||||
to learn more about what we're building.
|
||||
</Text>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={baseStyles.divider} />
|
||||
|
||||
<Text style={{ ...baseStyles.footerText, textAlign: 'left' }}>
|
||||
Submitted on {format(submittedDate, 'MMMM do, yyyy')}.
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CareersConfirmationEmail
|
||||
@@ -1,337 +0,0 @@
|
||||
import { Section, Text } from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { baseStyles, colors } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
|
||||
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 function CareersSubmissionEmail({
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
position,
|
||||
linkedin,
|
||||
portfolio,
|
||||
experience,
|
||||
location,
|
||||
message,
|
||||
submittedDate = new Date(),
|
||||
}: CareersSubmissionEmailProps) {
|
||||
return (
|
||||
<EmailLayout preview={`New Career Application from ${name}`} hideFooter showUnsubscribe={false}>
|
||||
<Text
|
||||
style={{
|
||||
...baseStyles.paragraph,
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textPrimary,
|
||||
}}
|
||||
>
|
||||
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: colors.bgOuter,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${colors.divider}`,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
margin: '0 0 16px 0',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Applicant Information
|
||||
</Text>
|
||||
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
width: '40%',
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Name:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Email:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
<a href={`mailto:${email}`} style={baseStyles.link}>
|
||||
{email}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{phone && (
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Phone:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
<a href={`tel:${phone}`} style={baseStyles.link}>
|
||||
{phone}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Position:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
{position}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Experience:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
{getExperienceLabel(experience)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Location:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
{location}
|
||||
</td>
|
||||
</tr>
|
||||
{linkedin && (
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
LinkedIn:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={linkedin}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
style={baseStyles.link}
|
||||
>
|
||||
View Profile
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{portfolio && (
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textMuted,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
Portfolio:
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={portfolio}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
style={baseStyles.link}
|
||||
>
|
||||
View Portfolio
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
|
||||
{/* Message */}
|
||||
<Section
|
||||
style={{
|
||||
marginTop: '24px',
|
||||
marginBottom: '24px',
|
||||
padding: '20px',
|
||||
backgroundColor: colors.bgOuter,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${colors.divider}`,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
margin: '0 0 12px 0',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: colors.textPrimary,
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
About Themselves
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
margin: '0',
|
||||
fontSize: '14px',
|
||||
color: colors.textPrimary,
|
||||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: baseStyles.fontFamily,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</Section>
|
||||
</EmailLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CareersSubmissionEmail
|
||||
@@ -1,2 +0,0 @@
|
||||
export { CareersConfirmationEmail } from './careers-confirmation-email'
|
||||
export { CareersSubmissionEmail } from './careers-submission-email'
|
||||
@@ -4,8 +4,6 @@ export * from './_styles'
|
||||
export * from './auth'
|
||||
// Billing emails
|
||||
export * from './billing'
|
||||
// Careers emails
|
||||
export * from './careers'
|
||||
// Shared components
|
||||
export * from './components'
|
||||
// Invitation emails
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
PlanWelcomeEmail,
|
||||
UsageThresholdEmail,
|
||||
} from '@/components/emails/billing'
|
||||
import { CareersConfirmationEmail, CareersSubmissionEmail } from '@/components/emails/careers'
|
||||
import {
|
||||
BatchInvitationEmail,
|
||||
InvitationEmail,
|
||||
@@ -225,44 +224,6 @@ export async function renderPaymentFailedEmail(params: {
|
||||
)
|
||||
}
|
||||
|
||||
export async function renderCareersConfirmationEmail(
|
||||
name: string,
|
||||
position: string
|
||||
): Promise<string> {
|
||||
return await render(
|
||||
CareersConfirmationEmail({
|
||||
name,
|
||||
position,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function renderCareersSubmissionEmail(params: {
|
||||
name: string
|
||||
email: string
|
||||
phone?: string
|
||||
position: string
|
||||
linkedin?: string
|
||||
portfolio?: string
|
||||
experience: string
|
||||
location: string
|
||||
message: string
|
||||
}): Promise<string> {
|
||||
return await render(
|
||||
CareersSubmissionEmail({
|
||||
name: params.name,
|
||||
email: params.email,
|
||||
phone: params.phone,
|
||||
position: params.position,
|
||||
linkedin: params.linkedin,
|
||||
portfolio: params.portfolio,
|
||||
experience: params.experience,
|
||||
location: params.location,
|
||||
message: params.message,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function renderWorkflowNotificationEmail(
|
||||
params: WorkflowNotificationEmailProps
|
||||
): Promise<string> {
|
||||
|
||||
@@ -328,6 +328,11 @@ const nextConfig: NextConfig = {
|
||||
source: '/team',
|
||||
destination: 'https://cal.com/emirkarabeg/sim-team',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/careers',
|
||||
destination: 'https://jobs.ashbyhq.com/sim',
|
||||
permanent: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user