Merge origin/master
This commit is contained in:
@@ -154,7 +154,7 @@ export function ContactForm() {
|
||||
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-zinc-700 flex-shrink-0">
|
||||
<Image
|
||||
src="/images/headshot.webp"
|
||||
alt="Damjan Savić"
|
||||
alt="Damjan Savić - Full-Stack Web Developer"
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-cover"
|
||||
@@ -178,7 +178,7 @@ export function ContactForm() {
|
||||
className="flex items-start gap-4"
|
||||
>
|
||||
<div className="p-3 bg-zinc-800/50 rounded-lg">
|
||||
<MapPin className="h-6 w-6 text-zinc-400" />
|
||||
<MapPin className="h-6 w-6 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-zinc-300 mb-1">
|
||||
@@ -197,7 +197,7 @@ export function ContactForm() {
|
||||
className="flex items-start gap-4"
|
||||
>
|
||||
<div className="p-3 bg-zinc-800/50 rounded-lg">
|
||||
<Phone className="h-6 w-6 text-zinc-400" />
|
||||
<Phone className="h-6 w-6 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-zinc-300 mb-1">
|
||||
@@ -219,7 +219,7 @@ export function ContactForm() {
|
||||
className="flex items-start gap-4"
|
||||
>
|
||||
<div className="p-3 bg-zinc-800/50 rounded-lg">
|
||||
<Mail className="h-6 w-6 text-zinc-400" />
|
||||
<Mail className="h-6 w-6 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-zinc-300 mb-1">
|
||||
@@ -253,7 +253,7 @@ export function ContactForm() {
|
||||
>
|
||||
<div className="bg-zinc-800/30 border border-zinc-800 rounded-xl p-6 md:p-8">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Mail className="h-5 w-5 text-zinc-400" />
|
||||
<Mail className="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
{t('contactForm.title')}
|
||||
</h2>
|
||||
@@ -276,10 +276,13 @@ export function ContactForm() {
|
||||
onChange={handleChange}
|
||||
placeholder={t('contactForm.name.placeholder')}
|
||||
disabled={isSubmitting}
|
||||
aria-required="true"
|
||||
aria-invalid={!!errors.name}
|
||||
aria-describedby={errors.name ? 'name-error' : undefined}
|
||||
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-400">{errors.name}</p>
|
||||
<p id="name-error" role="alert" className="text-sm text-red-400">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -295,10 +298,13 @@ export function ContactForm() {
|
||||
onChange={handleChange}
|
||||
placeholder={t('contactForm.email.placeholder')}
|
||||
disabled={isSubmitting}
|
||||
aria-required="true"
|
||||
aria-invalid={!!errors.email}
|
||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-400">{errors.email}</p>
|
||||
<p id="email-error" role="alert" className="text-sm text-red-400">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -314,10 +320,13 @@ export function ContactForm() {
|
||||
placeholder={t('contactForm.message.placeholder')}
|
||||
rows={6}
|
||||
disabled={isSubmitting}
|
||||
aria-required="true"
|
||||
aria-invalid={!!errors.message}
|
||||
aria-describedby={errors.message ? 'message-error' : undefined}
|
||||
className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50 resize-none"
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="text-sm text-red-400">{errors.message}</p>
|
||||
<p id="message-error" role="alert" className="text-sm text-red-400">{errors.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,9 +349,11 @@ export function ContactForm() {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className="flex items-center gap-2 p-3 bg-green-900/20 border border-green-900/50 rounded-lg"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" aria-hidden="true" />
|
||||
<p className="text-green-400 text-sm">
|
||||
{t('contactForm.successMessage')}
|
||||
</p>
|
||||
@@ -353,6 +364,8 @@ export function ContactForm() {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="p-3 bg-red-900/20 border border-red-900/50 rounded-lg"
|
||||
>
|
||||
<p className="text-red-400 text-sm">
|
||||
@@ -364,13 +377,15 @@ export function ContactForm() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
aria-busy={isSubmitting}
|
||||
aria-label={isSubmitting ? t('contactForm.submitting') : undefined}
|
||||
className="w-full h-12 bg-zinc-100 hover:bg-white text-zinc-900 font-medium rounded-lg transition-colors duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<Loader2 className="h-5 w-5 animate-spin" aria-hidden="true" />
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-5 w-5" />
|
||||
<Send className="h-5 w-5" aria-hidden="true" />
|
||||
{t('contactForm.submit')}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user