Add blog posts, cleanup unused files, update components

- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 11:42:11 +01:00
co-authored by Claude Opus 4.5
parent cba6c66ccc
commit 43484c5023
164 changed files with 60969 additions and 23346 deletions
+26 -11
View File
@@ -111,7 +111,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"
@@ -135,7 +135,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">
@@ -154,7 +154,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">
@@ -176,7 +176,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">
@@ -210,7 +210,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>
@@ -233,10 +233,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>
@@ -252,10 +255,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>
@@ -271,10 +277,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>
@@ -283,9 +292,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>
@@ -296,6 +307,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">
@@ -307,13 +320,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')}
</>
)}
+1 -1
View File
@@ -26,7 +26,7 @@ const Hero = async () => {
<div className="md:hidden absolute inset-x-0 bottom-0 h-[60%] bg-gradient-to-t from-zinc-950 via-zinc-950/80 via-40% to-transparent z-10 pointer-events-none" />
{/* Social Links - Top */}
<div className="absolute top-8 left-6 sm:left-12 lg:left-20 flex gap-4 z-20">
<div className="absolute top-8 left-6 sm:left-12 lg:left-20 flex gap-4 z-30">
<a
href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/"
target="_blank"
+6 -46
View File
@@ -2,13 +2,12 @@
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { motion, AnimatePresence } from 'framer-motion';
import { motion } from 'framer-motion';
import { Database, Server, Code2, Bot, Workflow, FileCode2 } from 'lucide-react';
interface Skill {
name: string;
level: number;
description: string;
}
const iconMap: Record<string, React.ReactNode> = {
@@ -25,7 +24,6 @@ const iconMap: Record<string, React.ReactNode> = {
const Skills = () => {
const t = useTranslations('pages.home.skills');
const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
const [skills, setSkills] = useState<Skill[]>([]);
useEffect(() => {
@@ -96,54 +94,16 @@ const Skills = () => {
{skills.map((skill, idx) => (
<motion.div
key={skill.name}
className="relative"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: idx * 0.1 }}
className="p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3"
>
<motion.div
className={`relative p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-zinc-700/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : ''
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
role="button"
>
<motion.div
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}
animate={{
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
scale: selectedSkill === skill.name ? 1.1 : 1
}}
transition={{ duration: 0.4 }}
>
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
</motion.div>
<span className="text-white text-sm text-center">{skill.name}</span>
<AnimatePresence>
{selectedSkill === skill.name && (
<motion.div
className="absolute left-1/2 top-0 -translate-x-1/2 bg-zinc-800/80 backdrop-blur-sm rounded-lg px-4 py-2 text-zinc-300 text-xs text-center shadow-xl pointer-events-none"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
style={{
whiteSpace: 'nowrap',
transform: 'translate(-50%, calc(-100% - 12px))',
zIndex: 999
}}
>
{skill.description}
<div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-zinc-800" />
</motion.div>
)}
</AnimatePresence>
</motion.div>
<div className="text-white">
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
</div>
<span className="text-white text-sm text-center">{skill.name}</span>
</motion.div>
))}
</div>
+6 -6
View File
@@ -15,15 +15,15 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
givenName: 'Damjan',
familyName: 'Savić',
jobTitle: locale === 'de'
? 'AI & Automation Specialist | Fullstack Entwickler'
? 'Fullstack Entwickler'
: locale === 'sr'
? 'AI & Automation Specialist | Fullstack Developer'
: 'AI & Automation Specialist | Fullstack Developer',
? 'Fullstack Developer'
: 'Fullstack Developer',
description: locale === 'de'
? 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.'
? 'Fullstack Entwicklung für Websites, Apps und SaaS mit Next.js, React und TypeScript.'
: locale === 'sr'
? 'Razvoj AI agenata i rešenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.'
: 'Remote AI developer building AI agents and automation solutions for clients in USA, UK and Europe. From voice AI platforms to autonomous web agents.',
? 'Fullstack razvoj za web stranice, aplikacije i SaaS sa Next.js, React i TypeScript.'
: 'Fullstack development for websites, apps and SaaS with Next.js, React and TypeScript.',
url: baseUrl,
image: `${baseUrl}/images/og-image.jpg`,
email: 'info@damjan-savic.com',