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:
@@ -20,7 +20,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const descriptions: Record<string, string> = {
|
||||
de: 'Impressum und rechtliche Informationen für die Portfolio-Website von Damjan Savić, KI Entwickler und Automation Specialist aus Köln.',
|
||||
en: 'Legal notice and contact information for Damjan Savić portfolio website, AI Developer and Automation Specialist.',
|
||||
sr: 'Pravno obavestenje i kontakt informacije za portfolio veb sajt Damjana Savica, AI Developer i Automation Specialist.',
|
||||
sr: 'Pravno obavestenje i kontakt informacije za portfolio veb sajt Damjana Savića, AI Developer i Automation Specialist.',
|
||||
};
|
||||
|
||||
const localePath = locale === 'de' ? '/imprint' : `/${locale}/imprint`;
|
||||
|
||||
@@ -31,15 +31,15 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
const titles: Record<Locale, string> = {
|
||||
de: 'Damjan Savić | AI & Automation Specialist aus Köln',
|
||||
en: 'Damjan Savić | AI & Automation Specialist from Cologne',
|
||||
sr: 'Damjan Savić | AI & Automation Specialist iz Kelna',
|
||||
de: 'Damjan Savić | Fullstack Developer aus Köln',
|
||||
en: 'Damjan Savić | Fullstack Developer from Cologne',
|
||||
sr: 'Damjan Savić | Fullstack Developer iz Kelna',
|
||||
};
|
||||
|
||||
const descriptions: Record<Locale, string> = {
|
||||
de: 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.',
|
||||
en: 'Building AI agents and automation solutions. From voice AI platforms to autonomous web agents.',
|
||||
sr: 'Razvoj AI agenata i resenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.',
|
||||
de: 'Fullstack Entwicklung für Websites, Apps und SaaS mit Next.js, React und TypeScript.',
|
||||
en: 'Fullstack development for websites, apps and SaaS with Next.js, React and TypeScript.',
|
||||
sr: 'Fullstack razvoj za web stranice, aplikacije i SaaS sa Next.js, React i TypeScript.',
|
||||
};
|
||||
|
||||
// Generate alternates using localeMetadata
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ import './globals.css';
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Damjan Savić',
|
||||
default: 'Damjan Savić | AI & Automation Specialist',
|
||||
default: 'Damjan Savić | Fullstack Developer',
|
||||
},
|
||||
description: 'AI & Automation Specialist - Building AI agents and automation solutions.',
|
||||
description: 'Fullstack Developer - Building websites, apps and SaaS with Next.js, React and TypeScript.',
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'),
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const SEO: React.FC<SEOProps> = ({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: 'Damjan Savić',
|
||||
alternateName: 'Damjan Savic',
|
||||
alternateName: 'Damjan Savić',
|
||||
url: siteUrl,
|
||||
image: `${siteUrl}${image}`,
|
||||
description: defaultDescription,
|
||||
|
||||
@@ -6,7 +6,7 @@ export function LocalBusinessSchema() {
|
||||
"@type": "LocalBusiness",
|
||||
"@id": "https://damjan-savic.com/#localbusiness",
|
||||
"name": "Damjan Savić - Senior Fullstack Developer & IT Consultant",
|
||||
"alternateName": ["CoderConda", "Damjan Savic IT Services"],
|
||||
"alternateName": ["CoderConda", "Damjan Savić IT Services"],
|
||||
"description": "Damjan Savić bietet professionelle IT-Dienstleistungen in Köln und Umgebung. Spezialisiert auf Enterprise Software Development, KI-Integration, Cloud Architecture und digitale Transformation. Damjan Savić entwickelt maßgeschneiderte Lösungen für Ihr Unternehmen.",
|
||||
"url": "https://damjan-savic.com",
|
||||
"logo": {
|
||||
|
||||
@@ -10,7 +10,7 @@ export function LocalizedPersonSchema() {
|
||||
"@type": "Person",
|
||||
"@id": "https://damjan-savic.com/#person",
|
||||
"name": currentLanguage === 'sr' ? "Дамјан Савић" : "Damjan Savić",
|
||||
"alternateName": currentLanguage === 'sr' ? ["Damjan Savić", "Damjan Savic"] : "Damjan Savic",
|
||||
"alternateName": currentLanguage === 'sr' ? ["Damjan Savić", "Damjan Savić"] : "Damjan Savić",
|
||||
"jobTitle": currentLanguage === 'de' ?
|
||||
["AI & Automation Specialist", "Process Automation Specialist", "Fullstack Entwickler"] :
|
||||
currentLanguage === 'sr' ?
|
||||
@@ -199,7 +199,7 @@ export function LocalizedWebsiteSchema() {
|
||||
currentLanguage === 'sr' ?
|
||||
"Дамјан Савић - AI & Automation Specialist" :
|
||||
"Damjan Savić - AI & Automation Specialist",
|
||||
"alternateName": ["Damjan Savic Portfolio"],
|
||||
"alternateName": ["Damjan Savić Portfolio"],
|
||||
"description": currentLanguage === 'de' ?
|
||||
"Offizielle Website von Damjan Savić - AI & Automation Specialist. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung mit n8n und Fullstack Development." :
|
||||
currentLanguage === 'sr' ?
|
||||
|
||||
@@ -6,7 +6,7 @@ export function PersonSchema() {
|
||||
"@type": "Person",
|
||||
"@id": "https://damjan-savic.com/#person",
|
||||
"name": "Damjan Savić",
|
||||
"alternateName": "Damjan Savic",
|
||||
"alternateName": "Damjan Savić",
|
||||
"jobTitle": ["Senior Fullstack Developer", "Digital Solutions Consultant", "Software Architect", "KI/AI Spezialist"],
|
||||
"description": "Damjan Savić ist ein Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln. Damjan Savić ist spezialisiert auf Python, JavaScript, React, Next.js, TypeScript und KI-Integration. Mit über 10 Jahren Erfahrung entwickelt Damjan Savić maßgeschneiderte Enterprise-Lösungen, moderne Web-Applikationen und innovative KI-gestützte Systeme für Unternehmen jeder Größe.",
|
||||
"url": "https://damjan-savic.com",
|
||||
|
||||
@@ -7,7 +7,7 @@ export function WebsiteSchema() {
|
||||
"@id": "https://damjan-savic.com/#website",
|
||||
"url": "https://damjan-savic.com",
|
||||
"name": "Damjan Savić - Senior Fullstack Developer & Digital Solutions Consultant",
|
||||
"alternateName": ["Damjan Savic Portfolio", "CoderConda"],
|
||||
"alternateName": ["Damjan Savić Portfolio", "CoderConda"],
|
||||
"description": "Offizielle Website von Damjan Savić - Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln. Spezialisiert auf Enterprise Software Development, KI-Integration, Cloud Architecture und moderne Web-Technologien. Entdecken Sie innovative Lösungen von Damjan Savić.",
|
||||
"publisher": {
|
||||
"@id": "https://damjan-savic.com/#person"
|
||||
|
||||
@@ -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')}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -7,7 +7,7 @@ export const meta = {
|
||||
keywords: [
|
||||
// Name & Branding
|
||||
'Damjan Savić',
|
||||
'Damjan Savic',
|
||||
'Damjan Savić',
|
||||
'CoderConda',
|
||||
|
||||
// Voice AI & Agenten
|
||||
|
||||
@@ -7,7 +7,7 @@ export const meta = {
|
||||
keywords: [
|
||||
// Name & Branding
|
||||
'Damjan Savić',
|
||||
'Damjan Savic',
|
||||
'Damjan Savić',
|
||||
'CoderConda',
|
||||
|
||||
// Voice AI & Agents
|
||||
|
||||
@@ -395,6 +395,7 @@
|
||||
"placeholder": "Ihre Nachricht an mich..."
|
||||
},
|
||||
"submit": "Nachricht senden",
|
||||
"submitting": "Nachricht wird gesendet...",
|
||||
"successMessage": "Vielen Dank für Ihre Nachricht! Ich werde mich so schnell wie möglich bei Ihnen melden.",
|
||||
"errorMessage": "Entschuldigung, beim Senden Ihrer Nachricht ist ein Fehler aufgetreten."
|
||||
}
|
||||
|
||||
@@ -410,6 +410,7 @@
|
||||
"placeholder": "Your message to me..."
|
||||
},
|
||||
"submit": "Send Message",
|
||||
"submitting": "Sending message...",
|
||||
"successMessage": "Thank you for your message! I will get back to you as soon as possible.",
|
||||
"errorMessage": "Sorry, there was an error sending your message."
|
||||
}
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
"placeholder": "Vasa poruka za mene..."
|
||||
},
|
||||
"submit": "Posalji poruku",
|
||||
"submitting": "Slanje poruke...",
|
||||
"successMessage": "Hvala vam na poruci! Javicu vam se sto je pre moguce.",
|
||||
"errorMessage": "Izvinite, doslo je do greske prilikom slanja vase poruke."
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const CURRENT_ROLE = {
|
||||
|
||||
export const PROFILE = {
|
||||
name: "Damjan Savić",
|
||||
title: "AI & Automation Specialist",
|
||||
subtitle: "Building AI agents and automation solutions",
|
||||
title: "Fullstack Developer",
|
||||
subtitle: "Building websites, apps and SaaS with Next.js, React and TypeScript",
|
||||
languages: ["German", "English", "Serbian", "French", "Spanish", "Russian"]
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user