First Commit - Portfolio Page

This commit is contained in:
2025-02-10 00:55:39 +01:00
commit bba0e331a8
239 changed files with 33355 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
import { useEffect, Suspense } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import AppRoutes from './routes';
import ErrorBoundary from './components/ErrorBoundary';
import CookieBanner from './components/CookieBanner';
import PageTransition from './components/PageTransition';
import { logPageView } from './utils/analytics';
import './styles/scrollbar.css';
function App() {
useEffect(() => {
logPageView();
// Stellt sicher, dass die Scrollbar von Anfang an konsistent ist
document.documentElement.classList.add('custom-scrollbar');
}, []);
return (
<HelmetProvider>
<ErrorBoundary>
<BrowserRouter>
<div className="flex flex-col min-h-screen bg-background text-foreground">
<Layout>
<ErrorBoundary>
<Suspense fallback={null}>
<PageTransition>
<AppRoutes />
</PageTransition>
</Suspense>
</ErrorBoundary>
</Layout>
<CookieBanner />
</div>
</BrowserRouter>
</ErrorBoundary>
</HelmetProvider>
);
}
export default App;
+56
View File
@@ -0,0 +1,56 @@
@layer base {
:root {
/* Primary Colors */
--color-primary: 105 117 101; /* #697565 - Sage green */
--color-primary-dark: 24 28 20; /* #181C14 - Dark moss */
/* Secondary Colors */
--color-secondary: 24 28 20; /* #181C14 - Dark moss */
--color-secondary-gray: 60 61 55; /* #3C3D37 - Charcoal */
--color-secondary-fuzzy: 70 91 80; /* #465B50 - Dark sage */
/* Text Colors */
--color-text: 236 223 204; /* #ECDFCC - Cream */
--color-text-light: 105 117 101; /* #697565 - Sage */
/* Navigation Colors */
--nav-bg: 24 28 20; /* Dark moss */
--nav-bg-transparent: 24 28 20 / 0.95; /* Dark moss with transparency */
--nav-text: 236 223 204; /* Cream */
--nav-text-hover: 70 91 80; /* Dark sage */
--nav-text-active: 70 91 80; /* Dark sage */
--nav-border: 60 61 55; /* Charcoal */
--nav-shadow: 0 0 0 / 0.1;
/* System Colors */
--background: 24 28 20; /* Dark moss */
--foreground: 236 223 204; /* Cream */
--card: 30 34 26; /* Slightly lighter dark moss */
--card-foreground: 236 223 204; /* Cream */
--popover: 30 34 26; /* Slightly lighter dark moss */
--popover-foreground: 236 223 204; /* Cream */
--primary: 105 117 101; /* Sage */
--primary-foreground: 236 223 204; /* Cream */
--secondary: 60 61 55; /* Charcoal */
--secondary-foreground: 236 223 204; /* Cream */
--muted: 60 61 55; /* Charcoal */
--muted-foreground: 105 117 101; /* Sage */
--accent: 70 91 80; /* Dark sage - neuer dunkelgrüner Akzent */
--accent-foreground: 236 223 204; /* Cream */
--destructive: 180 60 60; /* Muted red for destructive actions */
--destructive-foreground: 236 223 204; /* Cream */
--border: 60 61 55; /* Charcoal */
--input: 60 61 55; /* Charcoal */
--ring: 70 91 80; /* Dark sage */
--radius: 0.5rem;
}
}
+43
View File
@@ -0,0 +1,43 @@
// Alert.tsx
import React from 'react';
import { motion } from 'framer-motion';
import { CheckCircle2, XCircle } from 'lucide-react';
interface AlertProps {
variant?: 'success' | 'error';
children: React.ReactNode;
}
export const Alert: React.FC<AlertProps> = ({ variant = 'success', children }) => {
const variants = {
success: {
bg: 'bg-primary/10',
border: 'border-primary',
text: 'text-primary',
icon: <CheckCircle2 className="h-5 w-5" />
},
error: {
bg: 'bg-red-500/10',
border: 'border-red-500',
text: 'text-red-400',
icon: <XCircle className="h-5 w-5" />
}
};
const style = variants[variant];
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`${style.bg} ${style.border} ${style.text} border rounded-lg p-4 flex items-start space-x-3`}
>
<span className="flex-shrink-0">{style.icon}</span>
<div className="flex-1">{children}</div>
</motion.div>
);
};
export const AlertDescription: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="text-sm">{children}</div>;
};
+41
View File
@@ -0,0 +1,41 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
// Fügen Sie diese Funktion direkt in die Datei ein
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
+77
View File
@@ -0,0 +1,77 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
interface BreadcrumbsProps {
currentTitle?: string;
}
const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ currentTitle }) => {
const location = useLocation();
// Definiere die feste Struktur der Breadcrumbs
const generateBreadcrumbs = () => {
const segments = location.pathname.split('/').filter(Boolean);
// Basis-Breadcrumb-Struktur
const breadcrumbs = [];
// Portfolio ist immer der erste Level nach Home
if (segments.includes('portfolio')) {
breadcrumbs.push({
title: 'Portfolio',
path: '/portfolio',
isLast: segments.length === 1
});
}
// Wenn wir in einem Projekt sind, füge den Projekttitel hinzu
if (segments.length > 1 && currentTitle) {
breadcrumbs.push({
title: currentTitle,
path: location.pathname,
isLast: true
});
}
return breadcrumbs;
};
const breadcrumbs = generateBreadcrumbs();
// Wenn wir nur auf der Homepage sind, zeigen wir keine Breadcrumbs
if (location.pathname === '/') return null;
return (
<nav className="flex items-center gap-2 py-4 sm:py-6">
{/* Home ist immer der erste Link */}
<Link
to="/"
className="text-zinc-400 hover:text-white transition-colors duration-200"
>
Home
</Link>
{breadcrumbs.map((crumb) => (
<React.Fragment key={crumb.path}>
<ChevronRight className="h-4 w-4 text-zinc-600" />
{crumb.isLast ? (
<span className="text-white font-medium">
{crumb.title}
</span>
) : (
<Link
to={crumb.path}
className="text-zinc-400 hover:text-white transition-colors duration-200"
>
{crumb.title}
</Link>
)}
</React.Fragment>
))}
</nav>
);
};
export default Breadcrumbs;
+60
View File
@@ -0,0 +1,60 @@
// Button.tsx
import React from 'react';
import { Loader2 } from 'lucide-react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({
className = '',
variant = 'default',
size = 'md',
isLoading,
leftIcon,
rightIcon,
children,
disabled,
...props
}, ref) => {
const baseStyles = 'inline-flex items-center justify-center rounded-lg font-medium transition-all focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
default: 'bg-primary text-white hover:bg-primary/90',
outline: 'border border-primary text-primary hover:bg-primary hover:text-white',
ghost: 'text-primary hover:bg-primary/10'
};
const sizes = {
sm: 'h-9 px-4 text-sm',
md: 'h-12 px-6 text-base',
lg: 'h-14 px-8 text-lg'
};
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
disabled={isLoading || disabled}
{...props}
>
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<>
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2">{rightIcon}</span>}
</>
)}
</button>
);
}
);
Button.displayName = 'Button';
+84
View File
@@ -0,0 +1,84 @@
import * as React from "react"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
// Fügen Sie diese Funktion direkt in die Datei ein
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+36
View File
@@ -0,0 +1,36 @@
"use client"
import { Phone, Mail, ArrowRight } from "lucide-react"
import { Link } from "react-router-dom"
import { motion } from "framer-motion"
export function ContactBar() {
return (
<motion.div
initial={{ y: -50 }}
animate={{ y: 0 }}
className="bg-gradient-to-r from-cyan-900 to-slate-900 text-white py-2 px-4"
>
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div className="flex items-center space-x-6 text-sm">
<a href="tel:+1234567890" className="flex items-center space-x-2 hover:text-cyan-400 transition-colors">
<Phone className="h-4 w-4" />
<span>+1 234 567 890</span>
</a>
<a
href="mailto:info@damjan-savic.com"
className="flex items-center space-x-2 hover:text-cyan-400 transition-colors"
>
<Mail className="h-4 w-4" />
<span>info@damjan-savic.com</span>
</a>
</div>
<Link to="/contact" className="flex items-center space-x-1 text-sm hover:text-cyan-400 transition-colors group">
<span>Contact Us</span>
<ArrowRight className="h-4 w-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</motion.div>
)
}
+125
View File
@@ -0,0 +1,125 @@
import React, { useState } from 'react';
import { Send, AlertCircle } from 'lucide-react';
import { getCsrfToken } from '../utils/csrf';
import { rateLimiter } from '../utils/rateLimiting';
interface ContactFormProps {
onSubmit: (data: FormData) => Promise<void>;
}
const ContactForm: React.FC<ContactFormProps> = ({ onSubmit }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
// Check rate limiting
const clientIp = '127.0.0.1'; // In production, get this from the request
if (rateLimiter.isRateLimited(clientIp)) {
const timeToReset = Math.ceil(rateLimiter.getTimeToReset(clientIp) / 1000 / 60);
throw new Error(`Too many attempts. Please try again in ${timeToReset} minutes.`);
}
// Validate CSRF token
const formData = new FormData();
formData.append('name', name);
formData.append('email', email);
formData.append('message', message);
formData.append('csrf_token', getCsrfToken());
await onSubmit(formData);
setSuccess(true);
setName('');
setEmail('');
setMessage('');
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg flex items-center">
<AlertCircle className="h-5 w-5 mr-2" />
<span>{error}</span>
</div>
)}
{success && (
<div className="bg-green-500/10 border border-green-500/20 text-green-500 p-4 rounded-lg">
Message sent successfully!
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-medium text-white/80 mb-2">Name</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-gray-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-white/80 mb-2">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-gray-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
required
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-white/80 mb-2">Message</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={6}
className="w-full bg-gray-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
required
></textarea>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-orange-500 hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium py-3 rounded-lg transition-colors flex items-center justify-center"
>
{loading ? (
<span className="flex items-center">
<svg className="animate-spin h-5 w-5 mr-2" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Sending...
</span>
) : (
<span className="flex items-center">
<Send className="h-5 w-5 mr-2" />
Send Message
</span>
)}
</button>
</form>
);
};
export default ContactForm;
+46
View File
@@ -0,0 +1,46 @@
import { Phone, Mail, ArrowRight } from "lucide-react"
import { Link } from "react-router-dom"
import { motion } from "framer-motion"
export function ContactInfo() {
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg mx-4 mb-4"
>
<div className="space-y-4">
<h3 className="text-sm font-medium text-white tracking-wider uppercase">
Kontakt
</h3>
<a
href="tel:+1234567890"
className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group"
>
<Phone className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors duration-200" />
<span>+1 234 567 890</span>
</a>
<a
href="mailto:info@damjan-savic.com"
className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group"
>
<Mail className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors duration-200" />
<span>info@damjan-savic.com</span>
</a>
<Link
to="/contact"
className="flex items-center justify-between mt-6 px-4 py-2 bg-zinc-800/50
hover:bg-zinc-800 rounded-full text-sm text-zinc-400 hover:text-white
transition-all duration-200 group border border-zinc-800 hover:border-zinc-700"
>
<span className="tracking-wide">Kontakt aufnehmen</span>
<ArrowRight className="h-4 w-4 transform group-hover:translate-x-1 transition-transform duration-200" />
</Link>
</div>
</motion.div>
)
}
+43
View File
@@ -0,0 +1,43 @@
import CookieConsent from 'react-cookie-consent';
import { useTranslation } from 'react-i18next';
const CookieBanner = () => {
const { t } = useTranslation();
return (
<CookieConsent
location="bottom"
buttonText={t('common.cookies.accept')}
declineButtonText={t('common.cookies.decline')}
enableDeclineButton
style={{
background: '#1a1a1a',
borderTop: '1px solid rgba(255, 255, 255, 0.1)'
}}
buttonStyle={{
background: '#ff6b00',
color: 'white',
fontSize: '14px',
borderRadius: '6px',
padding: '8px 16px',
}}
declineButtonStyle={{
background: 'transparent',
border: '1px solid rgba(255, 255, 255, 0.2)',
color: 'white',
fontSize: '14px',
borderRadius: '6px',
padding: '8px 16px',
}}
expires={365}
onAccept={() => {
// Initialize analytics only after consent
window.initializeAnalytics();
}}
>
{t('common.cookies.message')}
</CookieConsent>
);
};
export default CookieBanner;
+61
View File
@@ -0,0 +1,61 @@
import { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
private handleRetry = () => {
this.setState({ hasError: false, error: undefined });
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[400px] flex items-center justify-center p-4">
<div className="text-center">
<AlertTriangle className="h-12 w-12 text-orange-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-4">Something went wrong</h2>
<p className="text-white/70 mb-6">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={this.handleRetry}
className="inline-flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-lg transition-colors"
>
<RefreshCw className="h-4 w-4" />
Try Again
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+45
View File
@@ -0,0 +1,45 @@
import { motion } from "framer-motion"
export function FloatingPaths({ position }: { position: number }) {
const paths = Array.from({ length: 36 }, (_, i) => ({
id: i,
d: `M-${380 - i * 5 * position} -${189 + i * 6}C-${
380 - i * 5 * position
} -${189 + i * 6} -${312 - i * 5 * position} ${216 - i * 6} ${
152 - i * 5 * position
} ${343 - i * 6}C${616 - i * 5 * position} ${470 - i * 6} ${
684 - i * 5 * position
} ${875 - i * 6} ${684 - i * 5 * position} ${875 - i * 6}`,
color: `rgba(var(--accent),${0.1 + i * 0.01})`,
width: 0.5 + i * 0.03,
}))
return (
<div className="absolute inset-0 pointer-events-none">
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none">
<title>Background Paths</title>
{paths.map((path) => (
<motion.path
key={path.id}
d={path.d}
stroke="currentColor"
strokeWidth={path.width}
strokeOpacity={0.1 + path.id * 0.01}
initial={{ pathLength: 0.3, opacity: 0.6 }}
animate={{
pathLength: 1,
opacity: [0.3, 0.6, 0.3],
pathOffset: [0, 1, 0],
}}
transition={{
duration: 20 + Math.random() * 10,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
/>
))}
</svg>
</div>
)
}
+115
View File
@@ -0,0 +1,115 @@
// components/Footer.tsx
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Mail, Linkedin, Github, MapPin } from 'lucide-react';
const Footer = () => {
const { t } = useTranslation();
const currentYear = new Date().getFullYear();
return (
<footer className="bg-zinc-900 pt-16 pb-8 px-4 sm:px-6">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
{/* Contact Section */}
<div className="space-y-4">
<h3 className="text-white text-lg font-semibold mb-4">
{t('footer.sections.contact.title')}
</h3>
<div className="flex items-center space-x-2 text-zinc-400 hover:text-white transition-colors">
<Mail size={18} />
<a
href={`mailto:${t('footer.sections.contact.email')}`}
className="hover:underline"
aria-label={t('footer.sections.contact.aria.emailLink')}
>
{t('footer.sections.contact.email')}
</a>
</div>
<div className="flex items-center space-x-2 text-zinc-400">
<MapPin size={18} />
<span aria-label={t('footer.sections.contact.aria.locationText')}>
{t('footer.sections.contact.location')}
</span>
</div>
</div>
{/* Navigation Section */}
<div className="space-y-4">
<h3 className="text-white text-lg font-semibold mb-4">
{t('footer.sections.navigation.title')}
</h3>
<ul className="space-y-2">
<li>
<Link to="/portfolio" className="text-zinc-400 hover:text-white transition-colors">
{t('footer.sections.navigation.links.portfolio')}
</Link>
</li>
<li>
<Link to="/blog" className="text-zinc-400 hover:text-white transition-colors">
{t('footer.sections.navigation.links.blog')}
</Link>
</li>
<li>
<Link to="/about" className="text-zinc-400 hover:text-white transition-colors">
{t('footer.sections.navigation.links.about')}
</Link>
</li>
<li>
<Link to="/contact" className="text-zinc-400 hover:text-white transition-colors">
{t('footer.sections.navigation.links.contact')}
</Link>
</li>
</ul>
</div>
{/* Social Media Section */}
<div className="space-y-4">
<h3 className="text-white text-lg font-semibold mb-4">
{t('footer.sections.social.title')}
</h3>
<div className="flex space-x-4">
<a
href="https://www.linkedin.com/in/damjan-savić-720288127/"
target="_blank"
rel="noopener noreferrer"
className="text-zinc-400 hover:text-white transition-colors"
aria-label={t('footer.sections.social.aria.linkedin')}
>
<Linkedin size={24} />
</a>
<a
href="https://github.com/damjan1996"
target="_blank"
rel="noopener noreferrer"
className="text-zinc-400 hover:text-white transition-colors"
aria-label={t('footer.sections.social.aria.github')}
>
<Github size={24} />
</a>
</div>
</div>
</div>
{/* Legal Section */}
<div className="border-t border-zinc-800 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-zinc-400 text-sm">
&copy; {currentYear} Damjan Savić. {t('footer.legal.copyright')}
</p>
<div className="flex space-x-6">
<Link to="/privacy" className="text-zinc-400 hover:text-white text-sm transition-colors">
{t('footer.legal.links.privacy')}
</Link>
<Link to="/terms" className="text-zinc-400 hover:text-white text-sm transition-colors">
{t('footer.legal.links.terms')}
</Link>
</div>
</div>
</div>
</div>
</footer>
);
};
export default Footer;
+61
View File
@@ -0,0 +1,61 @@
import { useState } from 'react';
import { ImageOff } from 'lucide-react';
declare module 'react' {
interface ImgHTMLAttributes<T> extends React.HTMLAttributes<T> {
fetchpriority?: 'high' | 'low' | 'auto';
}
}
interface ImageWithFallbackProps {
src: string;
alt: string;
className?: string;
sizes?: string;
loading?: 'lazy' | 'eager';
fetchpriority?: 'high' | 'low' | 'auto';
}
const ImageWithFallback: React.FC<ImageWithFallbackProps> = ({
src,
alt,
className = '',
sizes = '100vw',
loading = 'lazy',
fetchpriority = 'auto'
}) => {
const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
if (error) {
return (
<div className={`flex items-center justify-center bg-gray-900 ${className}`}>
<div className="text-center p-4">
<ImageOff className="h-8 w-8 mx-auto mb-2 text-gray-500" />
<span className="text-sm text-gray-500">{alt}</span>
</div>
</div>
);
}
return (
<div className="relative">
{isLoading && (
<div className={`absolute inset-0 bg-gray-900/50 animate-pulse ${className}`} />
)}
<img
src={src}
alt={alt}
className={`${className} ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`}
loading={loading}
fetchpriority={fetchpriority}
sizes={sizes}
onLoad={() => setIsLoading(false)}
onError={() => setError(true)}
decoding="async"
/>
</div>
);
};
export default ImageWithFallback;
+25
View File
@@ -0,0 +1,25 @@
// Input.tsx
import React from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: string;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className = '', error, ...props }, ref) => {
return (
<div className="w-full">
<input
className={`w-full h-12 bg-zinc-900/50 border border-white/10 rounded-lg px-4 text-white
placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-primary transition-all
disabled:opacity-50 disabled:cursor-not-allowed ${error ? 'border-red-500' : ''} ${className}`}
ref={ref}
{...props}
/>
{error && <p className="mt-2 text-sm text-red-400">{error}</p>}
</div>
);
}
);
Input.displayName = 'Input';
+15
View File
@@ -0,0 +1,15 @@
// Label.tsx
import React from 'react';
interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
required?: boolean;
}
export const Label: React.FC<LabelProps> = ({ children, required, className = '', ...props }) => {
return (
<label className={`block text-sm font-medium text-white/80 mb-2 ${className}`} {...props}>
{children}
{required && <span className="text-red-400 ml-1">*</span>}
</label>
);
};
+97
View File
@@ -0,0 +1,97 @@
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Globe } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const LanguageSwitcher: React.FC = () => {
const { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const languages = [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'sr', name: 'Srpski' },
];
const currentLanguage = languages.find(lang => lang.code === i18n.language)?.name || 'Language';
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
React.useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative" ref={dropdownRef}>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="flex items-center space-x-2 text-zinc-400 hover:text-white
px-4 py-2 rounded-full transition-colors duration-200
hover:bg-zinc-800/50 border border-transparent hover:border-zinc-800"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-label="Select language"
>
<Globe className="h-5 w-5" aria-hidden="true" />
<span className="text-sm">{currentLanguage}</span>
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute right-0 mt-2 w-48 rounded-lg overflow-hidden
border border-zinc-800 bg-zinc-900/95 backdrop-blur-sm
shadow-lg"
role="listbox"
aria-label="Languages"
onKeyDown={handleKeyDown}
>
<div className="py-1">
{languages.map((lang) => (
<motion.button
key={lang.code}
whileHover={{ backgroundColor: 'rgba(39, 39, 42, 0.5)' }}
className={`w-full text-left px-4 py-2 text-sm transition-colors duration-200
${i18n.language === lang.code
? 'bg-zinc-800 text-white'
: 'text-zinc-400 hover:text-white'
}`}
onClick={() => {
i18n.changeLanguage(lang.code);
setIsOpen(false);
}}
role="option"
aria-selected={i18n.language === lang.code}
>
{lang.name}
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default LanguageSwitcher;
+167
View File
@@ -0,0 +1,167 @@
// components/Layout.tsx
import { useEffect, useState, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, Home, User, Briefcase, BookOpen, Phone, LayoutDashboard } from "lucide-react";
import { supabase } from "../utils/supabaseClient";
import { ContactInfo } from "../components/ContactInfo";
import { NavLink } from "../components/NavLink";
import LanguageSwitcher from "./LanguageSwitcher";
import { Link } from "react-router-dom";
import Footer from "./Footer";
interface LayoutProps {
children: ReactNode;
}
const Layout = ({ children }: LayoutProps) => {
const { t } = useTranslation();
const [isAdmin, setIsAdmin] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const checkAdmin = async () => {
const { data, error } = await supabase.auth.getSession();
if (error) {
console.error("Error checking admin status:", error);
return;
}
if (data.session) {
const { user } = data.session;
const { data: adminData, error: adminError } = await supabase
.from("profiles")
.select("isAdmin")
.eq("id", user.id)
.single();
if (adminError) {
console.error("Error checking admin role:", adminError);
return;
}
setIsAdmin(adminData.isAdmin);
}
};
checkAdmin();
const handleScroll = () => {
setScrolled(window.scrollY > 0);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navigationLinks = [
{ path: "/", label: t("navigation.main.home"), icon: <Home className="h-5 w-5" /> },
{ path: "/about", label: t("navigation.main.about"), icon: <User className="h-5 w-5" /> },
{ path: "/portfolio", label: t("navigation.main.portfolio"), icon: <Briefcase className="h-5 w-5" /> },
{ path: "/blog", label: t("navigation.main.blog"), icon: <BookOpen className="h-5 w-5" /> },
{ path: "/contact", label: t("navigation.main.contact"), icon: <Phone className="h-5 w-5" /> },
...(isAdmin
? [
{
path: "/dashboard",
label: t("navigation.admin.dashboard"),
icon: <LayoutDashboard className="h-5 w-5" />,
},
]
: []),
];
return (
<div className="min-h-screen bg-zinc-900 flex flex-col">
<nav
className={`
fixed w-full z-50 transition-all duration-300
${scrolled
? "bg-zinc-900/80 backdrop-blur supports-[backdrop-filter]:bg-zinc-900/80"
: "bg-transparent"
}
`}
role="navigation"
aria-label="Main navigation"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<Link to="/" className="flex items-center space-x-2 group shrink-0">
<motion.img
src="/logo.png"
alt=""
className="h-8 w-auto"
whileHover={{ scale: 1.05 }}
transition={{ type: "spring", stiffness: 400, damping: 10 }}
/>
</Link>
<div className="hidden md:flex items-center gap-2">
{navigationLinks.map(({ path, label, icon }) => (
<NavLink key={path} to={path} icon={icon} label={label} className="text-sm" />
))}
<div className="ml-4 text-zinc-400 pl-2 border-l border-zinc-700">
<LanguageSwitcher />
</div>
</div>
<motion.button
className="md:hidden p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-controls="mobile-menu"
aria-label={isOpen ? "Close menu" : "Open menu"}
whileTap={{ scale: 0.95 }}
>
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</motion.button>
</div>
</div>
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 md:hidden"
onClick={() => setIsOpen(false)}
/>
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed right-0 top-16 h-[calc(100vh-4rem)] w-80 bg-zinc-900 shadow-xl md:hidden overflow-y-auto border-l border-zinc-800"
>
<div className="flex flex-col h-full">
<ContactInfo />
<div className="flex-1 py-4 px-4">
{navigationLinks.map(({ path, label, icon }) => (
<NavLink
key={path}
to={path}
icon={icon}
label={label}
onClick={() => setIsOpen(false)}
className="flex items-center w-full mb-1"
/>
))}
</div>
<div className="border-t border-zinc-800 p-6">
<div className="flex items-center justify-between">
<span className="text-sm text-zinc-500">Sprache ändern</span>
<LanguageSwitcher />
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</nav>
<main className="flex-1 pt-16">{children}</main>
<Footer />
</div>
);
};
export default Layout;
+17
View File
@@ -0,0 +1,17 @@
import React, { Suspense } from 'react';
import LoadingSpinner from './LoadingSpinner';
interface LazyComponentProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
const LazyComponent: React.FC<LazyComponentProps> = ({ children, fallback = <LoadingSpinner /> }) => {
return (
<Suspense fallback={fallback}>
{children}
</Suspense>
);
};
export default LazyComponent;
+102
View File
@@ -0,0 +1,102 @@
import { motion } from 'framer-motion';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const LoadingSpinner = ({ size = 'md', className = '' }: LoadingSpinnerProps) => {
const sizes = {
sm: 'w-8 h-8',
md: 'w-12 h-12',
lg: 'w-16 h-16'
};
return (
<div className={`flex items-center justify-center min-h-[200px] ${className}`}>
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
className={`relative ${sizes[size]}`}
>
{/* Base Triangle */}
<svg
viewBox="0 0 100 100"
className="absolute inset-0 w-full h-full"
>
<path
d="M50 10 L90 80 L10 80 Z"
className="fill-none stroke-zinc-800"
strokeWidth="4"
/>
</svg>
{/* Animated Triangle Segments */}
<svg
viewBox="0 0 100 100"
className="absolute inset-0 w-full h-full"
>
<motion.path
d="M50 10 L90 80"
className="stroke-white"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0.2, 1, 0.2]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.path
d="M90 80 L10 80"
className="stroke-white"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0.2, 1, 0.2]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
delay: 0.66
}}
/>
<motion.path
d="M10 80 L50 10"
className="stroke-white"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{
pathLength: [0, 1, 0],
opacity: [0.2, 1, 0.2]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
delay: 1.33
}}
/>
</svg>
{/* Optional inner gradient effect */}
<div className="absolute inset-0 bg-gradient-to-t from-zinc-900/50 to-transparent rounded-full" />
</motion.div>
</div>
);
};
export default LoadingSpinner;
+28
View File
@@ -0,0 +1,28 @@
// src/components/MDXProvider.tsx
import React from 'react';
import { MDXProvider } from '@mdx-js/react';
const components = {
h1: (props: React.ComponentProps<'h1'>) => (
<h1 className="text-3xl font-bold my-6" {...props} />
),
h2: (props: React.ComponentProps<'h2'>) => (
<h2 className="text-2xl font-bold my-4" {...props} />
),
h3: (props: React.ComponentProps<'h3'>) => (
<h3 className="text-xl font-bold my-3" {...props} />
),
p: (props: React.ComponentProps<'p'>) => (
<p className="my-4 text-muted-foreground" {...props} />
),
code: (props: React.ComponentProps<'code'>) => (
<code className="bg-muted px-1.5 py-0.5 rounded text-sm" {...props} />
),
pre: (props: React.ComponentProps<'pre'>) => (
<pre className="bg-muted p-4 rounded-lg my-4 overflow-x-auto" {...props} />
),
};
export function MDXLayout({ children }: { children: React.ReactNode }) {
return <MDXProvider components={components}>{children}</MDXProvider>;
}
+47
View File
@@ -0,0 +1,47 @@
// components/NavLink.tsx
import { Link, useLocation } from 'react-router-dom'
import { motion } from 'framer-motion'
import React from 'react'
interface NavLinkProps {
to: string
icon: React.ReactNode
label: string
onClick?: () => void
className?: string
}
export function NavLink({ to, icon, label, onClick, className }: NavLinkProps) {
const location = useLocation()
const isActive = location.pathname === to
return (
<Link
to={to}
onClick={onClick}
className={`
relative flex items-center gap-2 rounded-full py-2 px-4
transition-all duration-200 ease-in-out
${isActive ? 'text-white' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'}
${className}
`}
>
{icon}
<span className="text-sm tracking-wide">{label}</span>
{isActive && (
<motion.div
layoutId="activeIndicator"
className="absolute inset-0 rounded-full bg-zinc-800/50 -z-10"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: "spring",
stiffness: 300,
damping: 30
}}
/>
)}
</Link>
)
}
+126
View File
@@ -0,0 +1,126 @@
"use client"
import type React from "react"
import { useEffect, useState } from "react"
import { motion, AnimatePresence, cubicBezier } from "framer-motion"
import { useLocation } from "react-router-dom"
interface PageTransitionProps {
children: React.ReactNode
}
const customEase = cubicBezier(0.25, 0.1, 0.25, 1)
const Logo = () => (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: 1,
transition: { duration: 0.3, ease: customEase },
}}
exit={{
opacity: 0,
scale: 1.2,
transition: { duration: 0.3, ease: customEase },
}}
className="w-16 h-16"
>
<img
src="/logo.png"
alt="Logo"
className="w-full h-full object-contain filter brightness-200"
/>
</motion.div>
)
const PageTransition = ({ children }: PageTransitionProps) => {
const location = useLocation()
const [isTransitioning, setIsTransitioning] = useState(true)
const [isOverlayActive, setIsOverlayActive] = useState(true)
useEffect(() => {
setIsTransitioning(true)
setIsOverlayActive(true)
const overlayTimer = setTimeout(() => {
setIsOverlayActive(false)
}, 1200) // Overlay animation duration
const transitionTimer = setTimeout(() => {
setIsTransitioning(false)
}, 800)
return () => {
clearTimeout(overlayTimer)
clearTimeout(transitionTimer)
}
}, [location])
return (
<AnimatePresence mode="wait">
<motion.div key={location.pathname} className="relative">
{/* Transition Container */}
<div className={`fixed inset-0 pointer-events-none ${isOverlayActive ? 'z-50' : '-z-10'}`}>
{/* Overlay Animation */}
<motion.div
className="absolute inset-0 bg-zinc-900"
initial={{ opacity: 0 }}
animate={{
opacity: [0, 1, 1, 0],
transition: {
duration: 1.2,
times: [0, 0.3, 0.7, 1],
ease: customEase,
},
}}
/>
{/* Logo */}
<AnimatePresence>
{isTransitioning && (
<motion.div
className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: {
delay: 0.3,
duration: 0.3,
},
}}
exit={{
opacity: 0,
transition: {
duration: 0.3,
},
}}
>
<Logo />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Page Content */}
<motion.div
className="relative"
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.4,
delay: 0.8,
ease: customEase,
},
}}
exit={{ opacity: 0, y: -20, transition: { duration: 0.3 } }}
>
{children}
</motion.div>
</motion.div>
</AnimatePresence>
)
}
export default PageTransition
+55
View File
@@ -0,0 +1,55 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, ArrowRight } from 'lucide-react';
interface NavigationItem {
title: string;
slug: string;
}
interface PostNavigationProps {
previous?: NavigationItem;
next?: NavigationItem;
basePath: string;
}
const PostNavigation: React.FC<PostNavigationProps> = ({ previous, next, basePath }) => {
if (!previous && !next) return null;
return (
<nav className="border-t border-white/10 mt-12 pt-8">
<div className="flex justify-between">
{previous ? (
<Link
to={`${basePath}/${previous.slug}`}
className="group flex items-center text-white/60 hover:text-white transition-colors"
>
<ArrowLeft className="h-4 w-4 mr-2 transition-transform group-hover:-translate-x-1" />
<div>
<div className="text-sm text-white/40">Previous</div>
<div className="line-clamp-1">{previous.title}</div>
</div>
</Link>
) : (
<div />
)}
{next ? (
<Link
to={`${basePath}/${next.slug}`}
className="group flex items-center text-right text-white/60 hover:text-white transition-colors"
>
<div>
<div className="text-sm text-white/40">Next</div>
<div className="line-clamp-1">{next.title}</div>
</div>
<ArrowRight className="h-4 w-4 ml-2 transition-transform group-hover:translate-x-1" />
</Link>
) : (
<div />
)}
</div>
</nav>
);
};
export default PostNavigation
+171
View File
@@ -0,0 +1,171 @@
// components/ProjectContentTemplate.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Calendar, Building2, Clock, Tag } from 'lucide-react';
interface Section {
title: string;
description?: string;
content?: string;
points?: string[];
code?: string;
image?: string;
}
interface ProjectTranslation {
meta: {
title: string;
description: string;
date: string;
client: string;
duration: string;
technologies: string[];
};
content: {
intro: string;
challenge: Section;
solution: Section;
technical: Section;
implementation: Section;
results: Section;
conclusion: string;
};
}
interface ProjectContentTemplateProps {
translationKey: string;
fallbackData?: ProjectTranslation;
}
const ProjectContentTemplate: React.FC<ProjectContentTemplateProps> = ({
translationKey,
fallbackData
}) => {
// Nur 't' aus dem Hook extrahieren, da 'i18n' nicht verwendet wird
const { t } = useTranslation();
// Übersetzten Inhalt abrufen
const translatedContent = t(`${translationKey}`, {
returnObjects: true,
defaultValue: fallbackData
}) as ProjectTranslation;
const renderSection = (section: Section) => {
if (!section) return null;
return (
<div className="mb-12">
<h2 className="text-2xl font-bold text-white mb-4">
{section.title}
</h2>
{section.description && (
<p className="text-zinc-400 mb-6">
{section.description}
</p>
)}
{section.content && (
<div className="prose prose-invert max-w-none mb-6">
{section.content}
</div>
)}
{section.points && (
<ul className="list-disc list-inside text-zinc-300 space-y-2">
{section.points.map((point, index) => (
<li key={index} className="ml-4">
{point}
</li>
))}
</ul>
)}
{section.code && (
<pre className="bg-zinc-800/50 p-4 rounded-lg overflow-x-auto border border-zinc-800">
<code className="text-zinc-300">
{section.code}
</code>
</pre>
)}
{section.image && (
<div className="mt-6">
<img
src={section.image}
alt={section.title}
className="rounded-lg w-full"
loading="lazy"
/>
</div>
)}
</div>
);
};
return (
<article className="max-w-4xl mx-auto">
{/* Project Meta Information */}
<div className="mb-12">
<div className="flex flex-wrap items-center gap-4 mb-6 text-zinc-400">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
<time>{translatedContent.meta.date}</time>
</div>
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4" />
<span>{translatedContent.meta.client}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>{translatedContent.meta.duration}</span>
</div>
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4">
{translatedContent.meta.title}
</h1>
<p className="text-xl text-zinc-400">
{translatedContent.meta.description}
</p>
</div>
{/* Introduction */}
<div className="prose prose-invert max-w-none mb-12">
<p>{translatedContent.content.intro}</p>
</div>
{/* Main Sections */}
{renderSection(translatedContent.content.challenge)}
{renderSection(translatedContent.content.solution)}
{renderSection(translatedContent.content.technical)}
{renderSection(translatedContent.content.implementation)}
{renderSection(translatedContent.content.results)}
{/* Conclusion */}
{translatedContent.content.conclusion && (
<div className="prose prose-invert max-w-none mb-12">
<h2 className="text-2xl font-bold text-white mb-4">
{t('portfolio.sections.conclusion')}
</h2>
<p>{translatedContent.content.conclusion}</p>
</div>
)}
{/* Technologies */}
<div className="mt-12 pt-6 border-t border-zinc-800">
<div className="flex items-center gap-4">
<Tag className="h-4 w-4 text-zinc-400" />
<div className="flex flex-wrap gap-2">
{translatedContent.meta.technologies.map((tech) => (
<span
key={tech}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-800 rounded-full text-sm text-zinc-300"
>
{t(`technologies.${tech}`, tech)}
</span>
))}
</div>
</div>
</div>
</article>
);
};
export default ProjectContentTemplate;
+44
View File
@@ -0,0 +1,44 @@
interface ResponsiveImageProps {
src: string;
alt: string;
className?: string;
sizes?: string;
loading?: 'lazy' | 'eager';
fetchpriority?: 'high' | 'low' | 'auto';
}
export default function ResponsiveImage({
src,
alt,
className = '',
sizes = '100vw',
loading = 'lazy',
fetchpriority = 'auto'
}: ResponsiveImageProps) {
// Generate Unsplash responsive URLs
const generateSrcSet = (url: string) => {
if (!url.includes('unsplash.com')) return undefined;
const widths = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
return widths
.map((w) => {
const modifiedUrl = url.replace(/w=\d+/, `w=${w}`);
return `${modifiedUrl} ${w}w`;
})
.join(', ');
};
const srcSet = generateSrcSet(src);
return (
<img
src={src}
alt={alt}
className={className}
loading={loading}
fetchpriority={fetchpriority}
sizes={sizes}
srcSet={srcSet}
/>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom';
import { AlertTriangle, Home, ArrowLeft } from 'lucide-react';
const RouteErrorBoundary = () => {
const error = useRouteError();
const navigate = useNavigate();
let title = 'Something went wrong';
let message = 'An unexpected error occurred';
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
title = 'Page not found';
message = "The page you're looking for doesn't exist or has been moved.";
} else {
title = `Error ${error.status}`;
message = error.statusText || 'An error occurred while processing your request.';
}
} else if (error instanceof Error) {
message = error.message;
} else if (typeof error === 'string') {
message = error;
}
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-black">
<div className="max-w-md w-full text-center">
<AlertTriangle className="h-12 w-12 text-orange-500 mx-auto mb-4" />
<h1 className="text-2xl font-bold mb-4">{title}</h1>
<p className="text-white/70 mb-8">{message}</p>
<div className="flex items-center justify-center gap-4">
<button
onClick={() => navigate(-1)}
className="inline-flex items-center gap-2 bg-gray-800 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-black"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</button>
<button
onClick={() => navigate('/')}
className="inline-flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 focus:ring-offset-black"
>
<Home className="h-4 w-4" />
Home Page
</button>
</div>
</div>
</div>
);
};
export default RouteErrorBoundary;
+118
View File
@@ -0,0 +1,118 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
interface SEOProps {
title?: string;
description?: string;
image?: string;
article?: boolean;
schema?: object;
}
const SEO: React.FC<SEOProps> = ({
title,
description = 'JTL Integration Expert and Digital Commerce Specialist with extensive experience in e-commerce solutions and warehouse management systems.',
image = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
article = false,
schema,
}) => {
// Nur i18n wird benötigt t wurde entfernt, da es nicht genutzt wird.
const { i18n } = useTranslation();
const siteTitle = 'Damjan Savić';
const fullTitle = title ? `${title} | ${siteTitle}` : siteTitle;
const siteUrl = 'https://damjan-savic.com';
const currentUrl = typeof window !== 'undefined' ? window.location.href : siteUrl;
const currentLanguage = i18n.language;
// Default schema für die Website
const defaultSchema = {
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Damjan Savić',
url: siteUrl,
image: image,
description: description,
sameAs: [
'https://linkedin.com/in/damjansavic',
'https://github.com/damjansavic'
],
jobTitle: 'JTL Integration Expert',
worksFor: {
'@type': 'Organization',
name: 'Independent Consultant'
},
knowsAbout: [
'JTL-Wawi',
'E-commerce',
'Warehouse Management Systems',
'Digital Commerce',
'System Integration'
]
};
// Sicherstellen, dass supportedLngs ein Array ist, bevor .filter verwendet wird
const alternateUrls: Array<{ hrefLang: string; href: string }> = Array.isArray(i18n.options.supportedLngs)
? i18n.options.supportedLngs
.filter((lng: string) => lng !== 'cimode')
.map((lng: string) => ({
hrefLang: lng,
href: `${siteUrl}/${lng}${window.location.pathname}`,
}))
: [];
return (
<Helmet>
{/* Basic meta tags */}
<html lang={currentLanguage} />
<title>{fullTitle}</title>
<meta name="description" content={description} />
<meta name="image" content={image} />
<link rel="canonical" href={currentUrl} />
{/* Language alternates */}
{alternateUrls?.map(
({ hrefLang, href }: { hrefLang: string; href: string }) => (
<link key={hrefLang} rel="alternate" hrefLang={hrefLang} href={href} />
)
)}
<link rel="alternate" hrefLang="x-default" href={siteUrl} />
{/* Open Graph meta tags */}
<meta property="og:url" content={currentUrl} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:site_name" content={siteTitle} />
<meta property="og:locale" content={currentLanguage} />
{alternateUrls?.map(
({ hrefLang }: { hrefLang: string; href: string }) => (
<meta key={hrefLang} property="og:locale:alternate" content={hrefLang} />
)
)}
{/* Twitter Card meta tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{/* Additional meta tags */}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta
name="keywords"
content="JTL, E-commerce, Warehouse Management, Digital Commerce, System Integration, JTL-Wawi, WMS"
/>
<meta name="author" content="Damjan Savić" />
{/* Schema.org markup */}
<script type="application/ld+json">
{JSON.stringify(schema || defaultSchema)}
</script>
</Helmet>
);
};
export default SEO;
+44
View File
@@ -0,0 +1,44 @@
import { motion, useAnimation } from "framer-motion"
import { useEffect, useRef } from "react"
interface ScrollAnimationProps {
children: React.ReactNode
delay?: number
}
const ScrollAnimation: React.FC<ScrollAnimationProps> = ({ children, delay = 0 }) => {
const controls = useAnimation()
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
controls.start({ opacity: 1, y: 0, transition: { duration: 0.5, delay: delay } })
}
},
{
threshold: 0.5,
},
)
if (ref.current) {
observer.observe(ref.current)
}
return () => {
if (ref.current) {
observer.unobserve(ref.current)
}
}
}, [controls, delay])
return (
<motion.div ref={ref} initial={{ opacity: 0, y: 20 }} animate={controls}>
{children}
</motion.div>
)
}
export default ScrollAnimation
+104
View File
@@ -0,0 +1,104 @@
import React, { useState, useRef } from 'react';
import { Share2, X, Twitter, Facebook, Linkedin, Link as LinkIcon } from 'lucide-react';
import { useOnClickOutside } from '../hooks/useOnClickOutside';
interface ShareMenuProps {
title: string;
url: string;
description: string;
}
const ShareMenu: React.FC<ShareMenuProps> = ({ title, url, description }) => {
const [isOpen, setIsOpen] = useState(false);
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied'>('idle');
const menuRef = useRef<HTMLDivElement>(null);
useOnClickOutside(menuRef, () => setIsOpen(false));
const shareLinks = [
{
name: 'Twitter',
icon: Twitter,
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
label: 'Share on Twitter'
},
{
name: 'Facebook',
icon: Facebook,
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
label: 'Share on Facebook'
},
{
name: 'LinkedIn',
icon: Linkedin,
href: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&summary=${encodeURIComponent(description)}`,
label: 'Share on LinkedIn'
},
];
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(url);
setCopyStatus('copied');
setTimeout(() => setCopyStatus('idle'), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-gray-300 hover:text-white focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 focus:ring-offset-black transition-colors p-2 rounded-full hover:bg-white/10"
aria-expanded={isOpen}
aria-haspopup="menu"
aria-label="Share this content"
>
{isOpen ? <X className="h-5 w-5" aria-hidden="true" /> : <Share2 className="h-5 w-5" aria-hidden="true" />}
</button>
{isOpen && (
<div
className="absolute right-0 mt-2 w-48 rounded-lg bg-gray-900 shadow-lg ring-1 ring-black ring-opacity-5 z-50"
role="menu"
aria-orientation="vertical"
onKeyDown={handleKeyDown}
>
<div className="py-1">
{shareLinks.map((link) => (
<a
key={link.name}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-orange-500"
role="menuitem"
aria-label={link.label}
>
<link.icon className="h-4 w-4 mr-3" aria-hidden="true" />
{link.name}
</a>
))}
<button
onClick={copyToClipboard}
className="flex items-center w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-orange-500"
role="menuitem"
>
<LinkIcon className="h-4 w-4 mr-3" aria-hidden="true" />
{copyStatus === 'copied' ? 'Copied!' : 'Copy Link'}
</button>
</div>
</div>
)}
</div>
);
};
export default ShareMenu
+52
View File
@@ -0,0 +1,52 @@
import React from 'react';
interface SkeletonProps {
className?: string;
}
const Skeleton: React.FC<SkeletonProps> = ({ className }) => (
<div className={`animate-pulse bg-gray-700/50 rounded ${className}`}></div>
);
export const BlogPostSkeleton = () => (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<Skeleton className="h-8 w-3/4 mb-4" />
<Skeleton className="h-4 w-1/4 mb-8" />
<Skeleton className="h-[400px] w-full mb-8 rounded-xl" />
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
</div>
);
export const ProjectSkeleton = () => (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<Skeleton className="h-8 w-2/3 mb-4" />
<div className="flex gap-4 mb-8">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
</div>
<Skeleton className="h-[400px] w-full mb-8 rounded-xl" />
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-4/6" />
</div>
</div>
);
export const PortfolioSkeleton = () => (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<Skeleton className="h-8 w-48 mx-auto mb-12" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<div key={i} className="aspect-[4/3]">
<Skeleton className="w-full h-full rounded-xl" />
</div>
))}
</div>
</div>
);
+60
View File
@@ -0,0 +1,60 @@
import React from 'react';
import { motion } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
interface Skill {
name: string;
level: number;
category: string;
description: string;
}
interface SkillsMatrixProps {
skills: Skill[];
}
const SkillsMatrix: React.FC<SkillsMatrixProps> = ({ skills }) => {
const [ref, inView] = useInView({
triggerOnce: true,
threshold: 0.1,
});
const categories = Array.from(new Set(skills.map(skill => skill.category)));
return (
<div ref={ref} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{categories.map((category, categoryIndex) => (
<div key={category} className="bg-gray-900/50 p-6 rounded-xl">
<h3 className="text-xl font-semibold mb-6">{category}</h3>
<div className="space-y-6">
{skills
.filter(skill => skill.category === category)
.map((skill, skillIndex) => (
<div key={skill.name}>
<div className="flex justify-between mb-2">
<div className="group relative">
<span>{skill.name}</span>
<div className="absolute bottom-full left-0 mb-2 w-64 p-4 bg-gray-800 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<p className="text-sm text-white/80">{skill.description}</p>
</div>
</div>
<span>{skill.level}%</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<motion.div
className="h-full bg-orange-500"
initial={{ width: 0 }}
animate={inView ? { width: `${skill.level}%` } : { width: 0 }}
transition={{ duration: 1, delay: categoryIndex * 0.2 + skillIndex * 0.1 }}
/>
</div>
</div>
))}
</div>
</div>
))}
</div>
);
};
export default SkillsMatrix;
+23
View File
@@ -0,0 +1,23 @@
import React, { useState } from 'react';
const SkipToContent: React.FC = () => {
const [isFocused, setIsFocused] = useState(false);
return (
<a
href="#main-content"
className={`
fixed top-4 left-4 px-4 py-2 bg-orange-500 text-white rounded-lg
transform transition-transform duration-200
${isFocused ? 'translate-y-0' : '-translate-y-full'}
focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2
`}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
>
Skip to main content
</a>
);
};
export default SkipToContent
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import { Star, Quote } from 'lucide-react';
import ImageWithFallback from './ImageWithFallback';
interface TestimonialProps {
name: string;
role: string;
company: string;
image: string;
content: string;
rating: number;
}
const TestimonialCard: React.FC<TestimonialProps> = ({
name,
role,
company,
image,
content,
rating
}) => {
return (
<div className="bg-gray-900/50 p-6 rounded-xl relative">
<Quote className="absolute top-4 right-4 h-8 w-8 text-orange-500/20" />
<div className="flex items-center space-x-4 mb-4">
<ImageWithFallback
src={image}
alt={name}
className="w-12 h-12 rounded-full"
loading="lazy"
/>
<div>
<h3 className="font-semibold">{name}</h3>
<p className="text-sm text-white/60">{role} at {company}</p>
</div>
</div>
<div className="flex mb-4">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`h-4 w-4 ${i < rating ? 'text-orange-500' : 'text-gray-600'}`}
fill={i < rating ? 'currentColor' : 'none'}
/>
))}
</div>
<p className="text-white/80">{content}</p>
</div>
);
};
export default TestimonialCard;
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
error?: string;
}
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className = '', error, ...props }, ref) => {
return (
<div className="w-full">
<textarea
className={`w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3
text-white placeholder-white/40 focus:outline-none focus:ring-2
focus:ring-primary transition-all resize-none
disabled:opacity-50 disabled:cursor-not-allowed
${error ? 'border-red-500' : ''}
${className}`}
ref={ref}
{...props}
/>
{error && <p className="mt-2 text-sm text-red-400">{error}</p>}
</div>
);
}
);
Textarea.displayName = 'Textarea';
export default Textarea;
+32
View File
@@ -0,0 +1,32 @@
import { useState, useCallback } from 'react';
import { handleError } from '../utils/errorHandling';
interface AsyncState<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
export function useAsync<T>() {
const [state, setState] = useState<AsyncState<T>>({
data: null,
error: null,
loading: false,
});
const execute = useCallback(async (asyncFunction: () => Promise<T>) => {
setState({ data: null, error: null, loading: true });
try {
const data = await asyncFunction();
setState({ data, error: null, loading: false });
return data;
} catch (error) {
const handledError = handleError(error, 'Async Operation');
setState({ data: null, error: handledError, loading: false });
throw handledError;
}
}, []);
return { ...state, execute };
}
+20
View File
@@ -0,0 +1,20 @@
import { RefObject, useEffect } from 'react';
export function useOnClickOutside(ref: RefObject<HTMLElement>, handler: (event: MouseEvent | TouchEvent) => void) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
+26
View File
@@ -0,0 +1,26 @@
// src/hooks/useProjectData.ts
import { useTranslation } from 'react-i18next';
import { useState, useEffect } from 'react';
export const useProjectData = <T = unknown>(projectId: string): T | null => {
const { i18n } = useTranslation();
const [projectData, setProjectData] = useState<T | null>(null);
useEffect(() => {
const loadProjectData = async () => {
try {
const projectModule = await import(
`../i18n/locales/${i18n.language}/portfolio/projects/${projectId}`
);
setProjectData(projectModule[projectId] as T);
} catch (error) {
console.error(`Failed to load project data for ${projectId}:`, error);
setProjectData(null);
}
};
loadProjectData();
}, [projectId, i18n.language]);
return projectData;
};
+32
View File
@@ -0,0 +1,32 @@
// src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import translationDE from './locales/de';
import translationEN from './locales/en';
import translationSR from './locales/sr';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
de: {
translation: translationDE
},
en: {
translation: translationEN
},
sr: {
translation: translationSR
}
},
fallbackLng: 'en',
supportedLngs: ['en', 'de', 'sr'],
interpolation: {
escapeValue: false
}
});
export default i18n;
+175
View File
@@ -0,0 +1,175 @@
// src/i18n/locales/de/blog/index.ts
export const blog = {
meta: {
title: 'Blog - JTL Integration & Digital Solutions',
description: 'Insights und Best Practices zu JTL-WaWi, E-Commerce und digitaler Transformation',
header: {
title: 'Insights & Best Practices',
subtitle: 'Insights und Best Practices zu JTL-WaWi, E-Commerce und digitaler Transformation'
}
},
posts: {
'erp-integration-breuninger': {
title: 'ApparelMagic und TradeByte: Analyse komplexer Integrationen',
date: '2024-02-09',
excerpt: 'Detaillierte Analyse einer E-Commerce-Integration zwischen Apparel Magic und Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
content: {
intro: {
title: 'Apparel Magic meets Breuninger: Eine technische Analyse der TradeByte-Integration',
description: 'Die Integration von E-Commerce-Systemen mit etablierten Marktplätzen stellt Unternehmen vor besondere Herausforderungen. In diesem Artikel teile ich meine Erfahrungen aus einem komplexen Integrationsprojekt zwischen Apparel Magic als ERP-System und der Breuninger-Plattform über TradeByte.'
},
background: {
title: 'Projekthintergrund',
systems: {
erp: 'Apparel Magic als ERP-System für Produktdaten und Bestandsmanagement',
middleware: 'TradeByte als Middleware für die Breuninger-Plattform'
},
challenges: {
sync: 'Bidirektionale Synchronisation von Bestellungen',
customers: 'Automatisierte Kundenanlage',
delivery: 'Verwaltung von Lieferscheinen',
inventory: 'Bestandsmanagement in Echtzeit'
}
},
tech: {
title: 'Technische Architektur',
components: {
title: 'Zentrale Komponenten'
},
database: {
title: 'Middleware-Datenbank',
description: 'Die MariaDB-Datenbank fungiert als zentraler Datenspeicher für die Integration:'
}
},
api: {
title: 'API-Integrationen',
apparel_magic: {
title: 'Apparel Magic API',
description: 'Die REST-API von Apparel Magic wird für Kundenanlage und Bestandsmanagement verwendet:'
},
tradebyte: {
title: 'TradeByte Integration',
description: 'Die TradeByte-API verwendet eine Kombination aus REST und XML:'
}
},
order_process: {
title: 'Bestellprozess',
steps: {
fetch: 'Regelmäßiger Abruf neuer Bestellungen von TradeByte',
customers: 'Automatische Kundenanlage in Apparel Magic',
process: 'Bestellverarbeitung und Statusaktualisierung',
delivery: 'Generierung und Speicherung von Lieferscheinen'
},
delivery_notes: {
title: 'Lieferschein-Management'
}
},
challenges: {
title: 'Herausforderungen und Lösungen',
status: {
title: '1. Status-Management',
description: 'Eine besondere Herausforderung war das Management von Bestellstatus:'
},
error_handling: {
title: '2. Fehlerbehandlung und Monitoring',
description: 'Robuste Fehlerbehandlung war essentiell für den Produktivbetrieb:'
}
},
best_practices: {
title: 'Best Practices',
api: {
title: '1. API-Kommunikation',
items: {
retry: 'Implementierung von Retry-Mechanismen',
errors: 'Sorgfältiges Error Handling',
logging: 'Ausführliches Logging aller Kommunikation'
}
},
data: {
title: '2. Datenmanagement',
items: {
storage: 'Zentrale Datenhaltung in MariaDB',
transactions: 'Transaktionssicherheit bei kritischen Operationen',
validation: 'Regelmäßige Datenvalidierung'
}
},
automation: {
title: '3. Prozessautomatisierung',
items: {
customers: 'Automatisierte Kundenanlage',
status: 'Automatische Statusaktualisierungen',
delivery: 'Systematisches Lieferschein-Management'
}
}
},
lessons: {
title: 'Lessons Learned',
api: {
title: '1. API-Design',
items: {
docs: 'Gründliche API-Dokumentation ist essentiell',
auth: 'Verständnis der verschiedenen Authentifizierungsmethoden',
limits: 'Berücksichtigung von Rate Limits'
}
},
validation: {
title: '2. Datenvalidierung',
items: {
rules: 'Implementierung strenger Validierungsregeln',
edge_cases: 'Sorgfältige Handhabung von Sonderfällen',
cleanup: 'Automatische Datenbereinigung'
}
},
optimization: {
title: '3. Prozessoptimierung',
items: {
automation: 'Kontinuierliche Verbesserung der Automatisierung',
performance: 'Regelmäßige Performance-Überprüfungen',
monitoring: 'Proaktives Monitoring'
}
}
},
conclusion: {
title: 'Fazit',
requirements: {
understanding: 'Tiefes Verständnis beider Systeme',
implementation: 'Sorgfältige Implementierung der API-Kommunikation',
error_handling: 'Robuste Fehlerbehandlung',
monitoring: 'Kontinuierliches Monitoring'
},
results: 'Die entwickelte Lösung verarbeitet erfolgreich Bestellungen und ermöglicht eine nahtlose Integration zwischen den Systemen. Besonders die automatisierte Kundenanlage und das Lieferschein-Management haben sich als effizienzsteigernd erwiesen.'
}
}
},
},
ui: {
readMore: 'Weiterlesen',
loading: {
posts: 'Blogbeiträge werden geladen...',
post: 'Blogbeitrag wird geladen...'
},
errors: {
posts: 'Fehler beim Laden der Blogbeiträge',
post: 'Der gesuchte Blogbeitrag existiert nicht oder wurde verschoben.'
},
backToBlog: 'Zurück zum Blog',
publishedOn: 'Veröffentlicht am',
category: 'Kategorie',
tags: 'Tags',
notFound: {
title: 'Beitrag nicht gefunden',
message: 'Der gesuchte Blogbeitrag existiert nicht oder wurde verschoben.'
}
},
categories: {
'Integration': 'Integration',
'Entwicklung': 'Entwicklung',
'Automatisierung': 'Automatisierung'
}
} as const;
export type BlogConfig = typeof blog;
export type BlogPost = BlogConfig['posts'][keyof BlogConfig['posts']];
@@ -0,0 +1,138 @@
// src/i18n/locales/de/blog/posts/erp-integration-breuninger.ts
export const erpIntegrationBreuninger = {
posts: {
erp_integration: {
title: 'ApparelMagic und TradeByte: Analyse komplexer Integrationen',
date: '2024-02-09',
excerpt: 'Detaillierte Analyse einer E-Commerce-Integration zwischen Apparel Magic und Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
intro: {
title: 'Apparel Magic meets Breuninger: Eine technische Analyse der TradeByte-Integration',
description: 'Die Integration von E-Commerce-Systemen mit etablierten Marktplätzen stellt Unternehmen vor besondere Herausforderungen. In diesem Artikel teile ich meine Erfahrungen aus einem komplexen Integrationsprojekt zwischen Apparel Magic als ERP-System und der Breuninger-Plattform über TradeByte.'
},
background: {
title: 'Projekthintergrund',
systems: {
erp: 'Apparel Magic als ERP-System für Produktdaten und Bestandsmanagement',
middleware: 'TradeByte als Middleware für die Breuninger-Plattform'
},
challenges: {
sync: 'Bidirektionale Synchronisation von Bestellungen',
customers: 'Automatisierte Kundenanlage',
delivery: 'Verwaltung von Lieferscheinen',
inventory: 'Bestandsmanagement in Echtzeit'
}
},
tech: {
title: 'Technische Architektur',
components: {
title: 'Zentrale Komponenten'
},
database: {
title: 'Middleware-Datenbank',
description: 'Die MariaDB-Datenbank fungiert als zentraler Datenspeicher für die Integration:'
}
},
api: {
title: 'API-Integrationen',
apparel_magic: {
title: 'Apparel Magic API',
description: 'Die REST-API von Apparel Magic wird für Kundenanlage und Bestandsmanagement verwendet:'
},
tradebyte: {
title: 'TradeByte Integration',
description: 'Die TradeByte-API verwendet eine Kombination aus REST und XML:'
}
},
order_process: {
title: 'Bestellprozess',
steps: {
fetch: 'Regelmäßiger Abruf neuer Bestellungen von TradeByte',
customers: 'Automatische Kundenanlage in Apparel Magic',
process: 'Bestellverarbeitung und Statusaktualisierung',
delivery: 'Generierung und Speicherung von Lieferscheinen'
},
delivery_notes: {
title: 'Lieferschein-Management'
}
},
challenges: {
title: 'Herausforderungen und Lösungen',
status: {
title: '1. Status-Management',
description: 'Eine besondere Herausforderung war das Management von Bestellstatus:'
},
error_handling: {
title: '2. Fehlerbehandlung und Monitoring',
description: 'Robuste Fehlerbehandlung war essentiell für den Produktivbetrieb:'
}
},
best_practices: {
title: 'Best Practices',
api: {
title: '1. API-Kommunikation',
items: {
retry: 'Implementierung von Retry-Mechanismen',
errors: 'Sorgfältiges Error Handling',
logging: 'Ausführliches Logging aller Kommunikation'
}
},
data: {
title: '2. Datenmanagement',
items: {
storage: 'Zentrale Datenhaltung in MariaDB',
transactions: 'Transaktionssicherheit bei kritischen Operationen',
validation: 'Regelmäßige Datenvalidierung'
}
},
automation: {
title: '3. Prozessautomatisierung',
items: {
customers: 'Automatisierte Kundenanlage',
status: 'Automatische Statusaktualisierungen',
delivery: 'Systematisches Lieferschein-Management'
}
}
},
lessons: {
title: 'Lessons Learned',
api: {
title: '1. API-Design',
items: {
docs: 'Gründliche API-Dokumentation ist essentiell',
auth: 'Verständnis der verschiedenen Authentifizierungsmethoden',
limits: 'Berücksichtigung von Rate Limits'
}
},
validation: {
title: '2. Datenvalidierung',
items: {
rules: 'Implementierung strenger Validierungsregeln',
edge_cases: 'Sorgfältige Handhabung von Sonderfällen',
cleanup: 'Automatische Datenbereinigung'
}
},
optimization: {
title: '3. Prozessoptimierung',
items: {
automation: 'Kontinuierliche Verbesserung der Automatisierung',
performance: 'Regelmäßige Performance-Überprüfungen',
monitoring: 'Proaktives Monitoring'
}
}
},
conclusion: {
title: 'Fazit',
requirements: {
understanding: 'Tiefes Verständnis beider Systeme',
implementation: 'Sorgfältige Implementierung der API-Kommunikation',
error_handling: 'Robuste Fehlerbehandlung',
monitoring: 'Kontinuierliches Monitoring'
},
results: 'Die entwickelte Lösung verarbeitet erfolgreich Bestellungen und ermöglicht eine nahtlose Integration zwischen den Systemen. Besonders die automatisierte Kundenanlage und das Lieferschein-Management haben sich als effizienzsteigernd erwiesen.'
}
}
}
};
@@ -0,0 +1,253 @@
// src/i18n/locales/de/blog/posts/fullstack-development-timetracking.ts
export const fullstackDevelopmentTimetracking = {
meta: {
title: 'Fullstack-Entwicklung mit Python und React: Architektur unserer Zeiterfassungslösung',
date: '2024-02-09',
excerpt: 'Eine technische Deep-Dive in die Implementierung einer modernen Zeiterfassungslösung',
category: 'System Architecture',
coverImage: '/images/posts/fullstack-development-timetracking/cover.jpg',
tags: ['Python', 'React', 'TypeScript', 'MSSQL', 'System Design']
},
content: {
intro: {
title: 'Fullstack-Entwicklung mit Python und React: Architektur unserer Zeiterfassungslösung',
description: 'Die Entwicklung einer robusten Zeiterfassungslösung erfordert nicht nur technisches Know-how, sondern auch ein tiefes Verständnis für komplexe Geschäftsregeln und Benutzeranforderungen. In diesem Artikel teile ich unsere Erfahrungen bei der Implementierung einer modernen Fullstack-Zeiterfassungslösung mit Python und React.'
},
systemArchitecture: {
title: 'Systemarchitektur',
frontend: {
title: 'Frontend (Next.js + TypeScript)',
description: 'Die Frontend-Architektur basiert auf Next.js mit TypeScript und folgt einem komponenten-basierten Ansatz:',
code: {
types: `// types/TimeEntry.ts
interface TimeEntry {
id: number;
date: string;
checkIn: string;
checkOut: string | null;
userId: number;
status: 'complete' | 'incomplete';
}`,
component: `// components/TimeEntryForm.tsx
const TimeEntryForm: React.FC<TimeEntryFormProps> = ({ onSubmit }) => {
const [entry, setEntry] = useState<TimeEntry>({
date: new Date().toISOString().split('T')[0],
checkIn: new Date().toLocaleTimeString(),
checkOut: null,
status: 'incomplete'
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateTimeEntry(entry)) return;
try {
await onSubmit(entry);
} catch (error) {
console.error('Error submitting time entry:', error);
}
};
return (
<form onSubmit={handleSubmit} className='space-y-4'>
<DateInput
value={entry.date}
onChange={(date) => setEntry({ ...entry, date })}
/>
<TimeInput
value={entry.checkIn}
onChange={(time) => setEntry({ ...entry, checkIn: time })}
/>
{/* Additional form elements */}
</form>
);
};`
}
},
backend: {
title: 'Backend (Flask + MSSQL)',
description: 'Das Backend verwendet Flask für die API und MSSQL für die Datenpersistenz:',
code: {
models: `# models/time_entry.py
class TimeEntry(db.Model):
__tablename__ = 'Stundenzettel'
id = db.Column('ID', db.Decimal, primary_key=True)
personal_id = db.Column('Personal_ID', db.Decimal)
datum = db.Column('Datum', db.Date)
kommen = db.Column('Kommen', db.Time)
gehen = db.Column('Gehen', db.Time)
@validates('gehen')
def validate_checkout(self, key, value):
if value and value < self.kommen:
raise ValueError('Checkout time cannot be before checkin time')
return value`,
routes: `# routes/time_entries.py
@app.route('/api/time-entries', methods=['POST'])
@jwt_required
def create_time_entry():
data = request.get_json()
user_id = get_jwt_identity()
try:
validate_time_entry_creation(data, user_id)
entry = TimeEntry(
personal_id=user_id,
datum=data['date'],
kommen=data['checkIn'],
gehen=data.get('checkOut')
)
db.session.add(entry)
db.session.commit()
return jsonify(entry.to_dict()), 201
except ValidationError as e:
return jsonify({'error': str(e)}), 400`
}
}
},
businessLogic: {
title: 'Geschäftslogik-Implementierung',
validation: {
title: 'Validierung von Zeiteinträgen',
description: 'Die Validierungslogik stellt sicher, dass alle Geschäftsregeln eingehalten werden:',
code: `def validate_time_entry_creation(data: dict, user_id: int) -> None:
'''Validates a new time entry according to business rules.'''
# Check for existing incomplete entries
incomplete_entry = TimeEntry.query.filter_by(
personal_id=user_id,
gehen=None,
datum=data['date']
).first()
if incomplete_entry:
raise ValidationError('Cannot create new entry while incomplete entry exists')
# Check for time overlap with existing entries
overlapping_entry = TimeEntry.query.filter(
TimeEntry.personal_id == user_id,
TimeEntry.datum == data['date'],
TimeEntry.kommen <= data['checkIn'],
TimeEntry.gehen >= data['checkIn']
).first()
if overlapping_entry:
raise ValidationError('Time entry overlaps with existing entry')`
}
},
databaseDesign: {
title: 'Datenbankdesign',
description: 'Das MSSQL-Datenbankschema ist auf Effizienz und Integrität ausgelegt:',
code: `CREATE TABLE dbo.Stundenzettel (
ID decimal NOT NULL PRIMARY KEY,
Personal_ID decimal,
Datum date,
Kommen time,
Gehen time,
xStatus int,
xBenutzer nvarchar(15),
xDatum datetime,
xVersion timestamp
);
CREATE INDEX idx_personal_datum
ON dbo.Stundenzettel(Personal_ID, Datum);`
},
security: {
title: 'Sicherheitsimplementierung',
authentication: {
title: 'JWT-Authentifizierung',
code: `# auth/jwt_handler.py
from flask_jwt_extended import create_access_token
def authenticate_user(username: str, password: str) -> str:
user = Personal.query.filter_by(
Benutzername=username,
Passwort=password # In production, use proper password hashing
).first()
if not user:
raise AuthenticationError('Invalid credentials')
return create_access_token(identity=user.ID)`
}
},
bestPractices: {
title: 'Best Practices und Learnings',
points: [
{
title: 'Datenvalidierung auf mehreren Ebenen',
items: [
'Frontend-Validierung für sofortiges Feedback',
'Backend-Validierung für Geschäftsregeln',
'Datenbankconstraints für Datenintegrität'
]
},
{
title: 'Fehlerbehandlung',
items: [
'Strukturierte Fehlermeldungen',
'Benutzerfreundliche Fehlermeldungen im Frontend',
'Detailliertes Logging im Backend'
]
},
{
title: 'Performance-Optimierung',
items: [
'Indexierung kritischer Datenbankfelder',
'Frontend-Caching von Zeiteinträgen',
'Lazy Loading für historische Daten'
]
}
]
},
challenges: {
title: 'Herausforderungen und Lösungen',
timezones: {
title: '1. Zeitzonen-Handling',
description: 'Eine besondere Herausforderung war die korrekte Behandlung von Zeitzonen:',
code: `// utils/dateTime.ts
export const formatTimeForDisplay = (time: string): string => {
return new Date(\`1970-01-01T\${time}\`).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
};
export const formatTimeForAPI = (time: string): string => {
return new Date(\`1970-01-01T\${time}\`).toISOString().split('T')[1];
};`
},
concurrency: {
title: '2. Concurrent Updates',
description: 'Die Behandlung gleichzeitiger Updates erforderte spezielle Aufmerksamkeit:',
code: `from sqlalchemy import and_, or_
def update_time_entry(entry_id: int, data: dict) -> TimeEntry:
entry = TimeEntry.query.filter_by(id=entry_id).with_for_update().first()
if not entry:
raise NotFoundError('Time entry not found')
# Optimistic locking using version field
if entry.xVersion != data['version']:
raise ConcurrencyError('Entry was modified by another user')
entry.gehen = data.get('checkOut')
db.session.commit()
return entry`
},
offline: {
title: '3. Offline-Fähigkeit',
description: 'Für die Offline-Funktionalität implementierten wir eine Service Worker-Strategie:',
code: `// service-worker.ts
const CACHE_NAME = 'timetracking-v1';
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(response => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});`
}
},
conclusion: {
title: 'Fazit',
description: 'Die Entwicklung einer Zeiterfassungslösung erfordert sorgfältige Planung und Berücksichtigung zahlreicher Geschäftsregeln. Durch den Einsatz moderner Technologien wie Next.js, TypeScript und Flask konnten wir eine robuste und benutzerfreundliche Lösung implementieren.',
keyPoints: [
'Strikte Typisierung mit TypeScript',
'Umfassende Validierungslogik',
'Effizientes Datenbankdesign',
'Benutzerfreundliche Fehlerbehandlung'
],
results: 'Die Lösung ist seit mehreren Monaten erfolgreich im Einsatz und verarbeitet täglich hunderte von Zeiteinträgen zuverlässig.'
}
}
};
@@ -0,0 +1,204 @@
// src/i18n/locales/de/blog/posts/rfid-automation.ts
export const rfidAutomation = {
meta: {
title: 'RFID-Etiketten-Automatisierung: Von Datenbank zum Drucker',
date: '2024-02-09',
excerpt: 'Eine detaillierte technische Analyse der Implementierung eines automatisierten RFID-Etikettendruck-Systems mit Python',
category: 'System Integration',
coverImage: '/images/posts/rfid-automation/cover.jpg',
tags: ['RFID', 'Python', 'ZPL', 'Automation', 'Zebra Printer', 'EPC']
},
content: {
intro: {
title: 'RFID-Etiketten-Automatisierung: Von Datenbank zum Drucker',
description: 'In modernen Logistik- und Retail-Umgebungen ist die effiziente und fehlerfreie Etikettierung von Produkten mit RFID-Tags von entscheidender Bedeutung. In diesem Artikel teile ich meine Erfahrungen bei der Implementierung einer automatisierten Lösung für die Generierung und den Druck von RFID-Etiketten mit Chipkodierung.'
},
requirements: {
title: 'Projekthintergrund',
points: [
'Automatische Generierung von EPC-Codes (Electronic Product Code)',
'Erstellung von ZPL-Code für Zebra-Drucker',
'Direkte Netzwerkkommunikation mit dem Drucker',
'Integration mit bestehenden Produktdaten',
'Fehlerbehandlung und Logging'
]
},
implementation: {
title: 'Technische Umsetzung',
epcGeneration: {
title: 'EPC-Code Generierung',
description: 'Die EPC-Codes werden nach dem SGTIN-Standard (Serialized Global Trade Item Number) generiert:',
code: `def generate_encoded_epc(company_prefix, indicator, item_ref, serial):
sgtin = SGTIN(company_prefix, indicator, item_ref, serial)
return sgtin.encode()`
},
zplTemplates: {
title: 'ZPL-Template Management',
description: 'Für die unterschiedlichen Etikettentypen wurden ZPL-Templates implementiert:',
code: `def generate_zpl_code(artikelnr, description, ean, price, material, water_resistance, glass_type, encoded_epc):
formatted_price = f'{price:.2f}'
return f'''
CT~~CD,~CC^~CT~
^XA
~TA000
~JSN
^LT35
^MNW
^MTT
^PON
^PMN
^LH0,0
^JMA
^PR2,2
~SD23
^JUS
^LRN
^CI27
^PA0,1,1,0
^RS8,,,3
^XZ
^XA
^MMT
^PW413
^LL531
^LS-24
^FT150,57^A0N,33,33^FH\\^CI27^FD{artikelnr}^FS^CI27
^FT44,86^A0N,21,20^FH\\^CI27^FD{description}^FS^CI27
^FT130,141^A0N,50,51^FH\\^CI27^FD{formatted_price} €^FS^CI27
^FT167,175^A0N,21,20^FH\\^CI27^FD{material}^FS^CI27
^FT185,201^A0N,21,20^FH\\^CI27^FD{water_resistance}^FS^CI27
^FT155,227^A0N,21,20^FH\\^CI27^FD{glass_type}^FS^CI27
^BY3,2,113^FT73,420^BEN,,Y,N
^FH\\^FD{ean}^FS
^RFW,H,1,2,1^FD3000^FS
^RFW,H,2,12,1^FD{encoded_epc}^FS
^PQ1,0,1,Y
^XZ'''`
},
printerCommunication: {
title: 'Netzwerkkommunikation mit dem Drucker',
description: 'Die Kommunikation mit dem Zebra-Drucker erfolgt über TCP/IP:',
code: `def send_to_printer(data, printer_ip='192.168.68.50', printer_port=9100):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((printer_ip, printer_port))
sock.sendall(data.encode('utf-8'))
logger.info('Daten erfolgreich an den Drucker gesendet')
except Exception as e:
logger.error(f'Fehler beim Senden der Daten an den Drucker: {e}')`
},
serialNumberManagement: {
title: 'Datenprozessierung und Seriennummernverwaltung',
description: 'Die Verwaltung von Seriennummern ist kritisch für die Eindeutigkeit der EPC-Codes:',
code: `def initialize_serial_number():
global current_serial_number
try:
serial_log_content = SERIAL_NUMBER_LOG_PATH.read_text().strip()
current_serial_number = int(serial_log_content)
except (FileNotFoundError, ValueError):
current_serial_number = START_SERIAL_NUMBER
def get_next_serial_number():
global current_serial_number
current_serial_number += 1
return current_serial_number
def finalize_serial_number():
SERIAL_NUMBER_LOG_PATH.write_text(str(current_serial_number))`
}
},
features: {
title: 'Implementierte Features',
errorHandling: {
title: '1. Robuste Fehlerbehandlung',
points: [
'Validierung der Eingabedaten',
'Überprüfung der Netzwerkverbindung',
'Persistenz von Seriennummern',
'Detailliertes Logging aller Operationen'
]
},
configurability: {
title: '2. Konfigurierbarkeit',
points: [
'Drucker-IP und Port einstellbar',
'Anpassbare ZPL-Templates',
'Flexible EPC-Code-Generierung',
'Konfigurierbare Seriennummernbereiche'
]
},
scalability: {
title: '3. Skalierbarkeit',
points: [
'Batch-Verarbeitung von Produktdaten',
'Parallele Druckaufträge möglich',
'Effizientes Ressourcenmanagement',
'Modularer Code für einfache Erweiterbarkeit'
]
}
},
bestPractices: {
title: 'Best Practices',
dataValidation: {
title: '1. Datenvalidierung',
points: [
'Strict typing für kritische Datenfelder',
'Überprüfung von Pflichtfeldern',
'Format- und Plausibilitätsprüfungen'
]
},
faultTolerance: {
title: '2. Fehlertoleranz',
points: [
'Automatische Wiederholungsversuche',
'Graceful Degradation',
'Detaillierte Fehlerprotokolle'
]
},
maintainability: {
title: '3. Wartbarkeit',
points: [
'Modularer Code-Aufbau',
'Ausführliche Dokumentation',
'Klare Trennung von Konfiguration und Code'
]
}
},
lessonsLearned: {
title: 'Lessons Learned',
zplSpecifics: {
title: '1. ZPL-Spezifika',
points: [
'Genaue Kenntnis der ZPL-Spezifikationen notwendig',
'Sorgfältige Validierung der generierten ZPL-Codes',
'Regelmäßige Tests mit verschiedenen Druckermodellen'
]
},
rfidStandards: {
title: '2. RFID-Standards',
points: [
'Einhaltung der EPC-Standards kritisch',
'Sorgfältige Verwaltung von Seriennummern',
'Validierung der generierten EPC-Codes'
]
},
networkCommunication: {
title: '3. Netzwerkkommunikation',
points: [
'Robuste Fehlerbehandlung bei Netzwerkproblemen',
'Timeouts und Wiederholungsversuche',
'Pufferung von Druckaufträgen'
]
}
},
conclusion: {
title: 'Fazit',
description: 'Die implementierte Lösung ermöglicht eine effiziente und zuverlässige Automatisierung des RFID-Etikettierungsprozesses. Durch die Kombination von EPC-Code-Generierung, ZPL-Template-Management und direkter Druckerkommunikation wurde ein robustes System geschaffen, das sich in der Praxis bewährt hat.',
keyPoints: [
'Gründliche Planung der Systemarchitektur',
'Umfassende Fehlerbehandlung',
'Sorgfältige Dokumentation',
'Regelmäßige Tests und Validierung'
],
results: 'Die Lösung läuft seit mehreren Monaten stabil im Produktivbetrieb und verarbeitet täglich hunderte von Etiketten.'
}
}
};
+99
View File
@@ -0,0 +1,99 @@
export const common = {
// SEO & Meta
siteTitle: 'Damjan Savić - Digital Solutions & JTL Integration',
siteDescription: 'Entwickler individueller Lösungen zur Automatisierung von Prozessen',
// Navigation
nav: {
home: 'Start',
about: 'Über mich',
services: 'Leistungen',
portfolio: 'Portfolio',
blog: 'Blog',
contact: 'Kontakt'
},
// Common Actions
actions: {
readMore: 'Weiterlesen',
viewMore: 'Mehr anzeigen',
viewAll: 'Alle anzeigen',
back: 'Zurück',
close: 'Schließen',
submit: 'Absenden',
loading: 'Lädt...'
},
// UI Components
ui: {
published: 'Veröffentlicht am',
lastUpdated: 'Zuletzt aktualisiert am',
category: 'Kategorie',
categories: 'Kategorien',
tag: 'Tag',
tags: 'Tags',
author: 'Autor',
minuteRead: 'Minuten Lesezeit',
tableOfContents: 'Inhaltsverzeichnis'
},
// Error Messages
errors: {
generic: 'Ein Fehler ist aufgetreten',
notFound: 'Seite nicht gefunden',
loading: 'Fehler beim Laden',
return: 'Zurück zur Startseite'
},
// Loading States
loading: {
content: 'Inhalt wird geladen',
page: 'Seite wird geladen',
processing: 'Wird verarbeitet'
},
// Dates
dates: {
months: {
long: [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
],
short: [
'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'
]
},
weekdays: {
long: [
'Sonntag', 'Montag', 'Dienstag', 'Mittwoch',
'Donnerstag', 'Freitag', 'Samstag'
],
short: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
}
},
// Social Media
social: {
followMe: 'Folgen Sie mir auf',
shareOn: 'Teilen auf',
linkedin: 'LinkedIn',
github: 'GitHub',
email: 'E-Mail'
},
// Contact Form
contact: {
title: 'Kontakt',
name: 'Name',
email: 'E-Mail',
message: 'Nachricht',
submit: 'Nachricht senden',
success: 'Ihre Nachricht wurde erfolgreich gesendet',
error: 'Es gab einen Fehler beim Senden Ihrer Nachricht'
},
// Accessibility
a11y: {
skipToContent: 'Zum Hauptinhalt springen',
menuOpen: 'Menü öffnen',
menuClose: 'Menü schließen',
darkMode: 'Dark Mode umschalten',
carousel: {
next: 'Nächstes Bild',
previous: 'Vorheriges Bild'
}
}
} as const;
export type CommonTranslations = typeof common;
+9
View File
@@ -0,0 +1,9 @@
const cookiebanner = {
cookies: {
message: "Diese Website verwendet Cookies, um Ihr Browsing-Erlebnis zu verbessern. Die Daten werden nicht an Dritte weitergegeben.",
accept: "Cookies akzeptieren",
decline: "Ablehnen"
}
};
export default cookiebanner;
+40
View File
@@ -0,0 +1,40 @@
// src/i18n/locales/de/footer.ts
const footer = {
sections: {
contact: {
title: "Kontakt",
email: "info@damjan-savic.com",
location: "Köln, Deutschland",
aria: {
emailLink: "E-Mail senden",
locationText: "Standort"
}
},
navigation: {
title: "Navigation",
links: {
portfolio: "Portfolio",
blog: "Blog",
about: "Über mich",
contact: "Kontakt"
}
},
social: {
title: "Social Media",
aria: {
linkedin: "Besuchen Sie mein LinkedIn Profil",
github: "Besuchen Sie mein GitHub Profil"
}
}
},
legal: {
copyright: "Alle Rechte vorbehalten",
links: {
privacy: "Datenschutz",
terms: "Impressum"
}
}
} as const;
export default footer;
export type FooterTranslations = typeof footer;
+109
View File
@@ -0,0 +1,109 @@
// src/i18n/locales/de/index.ts
// Blog post translations
import { erpIntegrationBreuninger } from './blog/posts/erp-integration-breuninger';
import { fullstackDevelopmentTimetracking } from './blog/posts/fullstack-development-timetracking';
import { rfidAutomation as blogRfidAutomation } from './blog/posts/rfid-automation';
import { blog } from './blog/index';
// Page translations
import { about } from './pages/about/index';
import { contact } from './pages/contact/index';
import { dashboard } from './pages/dashboard';
import { home } from './pages/home';
import { login } from './pages/login';
import { notfound } from './pages/notfound';
import { portfolio as portfolioPage } from './pages/portfolio';
import { privacy } from './pages/privacy';
import { terms } from './pages/terms';
// Portfolio project translations
import { aiDataReader } from './portfolio/projects/ai-data-reader';
import { portfolio } from './portfolio/index';
// Common translations and configurations
import { common } from './common';
import { meta } from './meta';
import { navigation } from './navigation';
// Footer translations
import footer from './footer';
// Zusammenführung aller Übersetzungsobjekte
const translations = {
// Pages
pages: {
about,
contact,
dashboard,
home,
login,
notfound,
portfolio: portfolioPage,
privacy,
terms,
},
// Blog
blog: {
...blog,
posts: {
erpIntegrationBreuninger,
fullstackDevelopmentTimetracking,
rfidAutomation: blogRfidAutomation,
},
},
// Portfolio
portfolio: {
...portfolio,
projects: {
aiDataReader,
},
},
// Footer
footer,
// Gemeinsame Konfigurationen
common: {
...common,
cookies: {
message: "Diese Website verwendet Cookies, um Ihr Browsing-Erlebnis zu verbessern. Die Daten werden nicht an Dritte weitergegeben.",
accept: "Cookies akzeptieren",
decline: "Ablehnen"
}
},
meta,
navigation,
} as const;
// Default-Export: komplettes Übersetzungsobjekt
export default translations;
// Typdefinition der Übersetzungen
export type Translations = typeof translations;
// Re-Exports für den direkten Zugriff auf einzelne Module
export {
// Pages
about,
contact,
dashboard,
home,
login,
notfound,
portfolioPage as portfolioPageTranslations,
privacy,
terms,
// Blog
blog,
erpIntegrationBreuninger,
fullstackDevelopmentTimetracking,
blogRfidAutomation as blogRfidAutomationPost,
// Portfolio
portfolio,
aiDataReader,
// Footer
footer,
// Common
common,
meta,
navigation,
};
+93
View File
@@ -0,0 +1,93 @@
export const meta = {
site: {
name: 'Damjan Savić',
title: 'Damjan Savić - Digital Solutions & JTL Integration',
subtitle: 'Entwickler individueller Lösungen zur Automatisierung von Prozessen',
description: 'Mit einem Hintergrund in der Softwareentwicklung und Expertise in der digitalen Transformation unterstütze ich Unternehmen bei der Optimierung und Automatisierung ihrer Geschäftsprozesse.',
keywords: [
'JTL Integration',
'E-Commerce Lösungen',
'Prozessautomatisierung',
'Digitale Beratung',
'ERP Systeme',
'Python Entwicklung',
'Systemintegration',
'Digitales Marketing'
].join(', ')
},
company: {
name: 'CoderConda',
shortName: 'CoderConda',
description: 'Ihr JTL-Servicepartner für schnelle, verbindliche und kosteneffiziente Lösungen.',
address: 'Rotdornallee',
city: 'Köln',
postalCode: '50999',
country: 'Deutschland',
phone: '+49 175 695 0979',
email: 'info@damjan-savic.com',
website: 'www.damjan-savic.com'
},
author: {
name: 'Damjan Savić',
role: 'Consultant Digital Solutions',
company: 'CoderConda',
location: 'Köln, Deutschland',
email: 'info@damjan-savic.com',
website: 'https://www.damjan-savic.com',
languages: [
{ code: 'en', level: 'C2', name: 'Englisch' },
{ code: 'sr', level: 'C2', name: 'Serbisch' },
{ code: 'de', level: 'C2', name: 'Deutsch' },
{ code: 'fr', level: 'B2', name: 'Französisch' },
{ code: 'es', level: 'B2', name: 'Spanisch' },
{ code: 'ru', level: 'A1', name: 'Russisch' }
]
},
social: {
linkedin: 'https://www.linkedin.com/in/damjansavic/',
github: 'https://github.com/damjansavic',
email: 'info@damjan-savic.com'
},
expertise: {
areas: [
'JTL Integration & Beratung',
'Prozessautomatisierung',
'E-Commerce Entwicklung',
'Systemintegration',
'ERP-Systeme',
'Digitale Transformation'
],
technologies: [
'Python',
'React/Next.js',
'MariaDB',
'Docker',
'JTL-WaWi',
'Shopify',
'Shopware'
]
},
seo: {
defaultLanguage: 'de',
alternateLanguages: ['en', 'sr'],
robots: 'index, follow',
googleSiteVerification: '', // Falls benötigt
og: {
type: 'website',
locale: 'de_DE',
siteName: 'Damjan Savić - Digital Solutions',
images: {
url: '/images/og-image.jpg',
width: 1200,
height: 630,
alt: 'Damjan Savić - Digital Solutions & JTL Integration'
}
},
twitter: {
card: 'summary_large_image',
site: '@damjansavic',
creator: '@damjansavic'
}
}
} as const;
export type MetaConfig = typeof meta;
+79
View File
@@ -0,0 +1,79 @@
// C:\Development\Damjan Savic\Portfolio\src\i18n\locales\de\navigation.ts
export const navigation = {
// Hauptnavigation
main: {
home: 'Startseite',
about: 'Über mich',
portfolio: 'Portfolio',
blog: 'Blog',
contact: 'Kontakt'
},
// Dashboard und Admin
admin: {
login: 'Anmelden',
dashboard: 'Dashboard',
logout: 'Abmelden'
},
// Footer Navigation
footer: {
legal: {
privacy: 'Datenschutz',
terms: 'AGB',
imprint: 'Impressum'
},
sections: {
portfolio: 'Portfolio',
blog: 'Blog',
contact: 'Kontakt'
}
},
// Seitennavigation
sections: {
experience: 'Erfahrung',
skills: 'Fähigkeiten',
projects: 'Projekte',
about: 'Über mich'
},
// Breadcrumbs und zurück-Navigation
navigation: {
backTo: {
home: 'Zurück zur Startseite',
portfolio: 'Zurück zum Portfolio',
blog: 'Zurück zum Blog'
},
breadcrumbs: {
home: 'Startseite',
portfolio: 'Portfolio',
blog: 'Blog',
about: 'Über mich',
contact: 'Kontakt'
}
},
// Mobile Navigation
mobile: {
menu: 'Menü',
close: 'Schließen',
open: 'Menü öffnen'
},
// URLs (für dynamische Route-Generierung)
urls: {
home: '/',
about: '/about',
portfolio: '/portfolio',
blog: '/blog',
contact: '/contact',
login: '/login',
dashboard: '/dashboard',
privacy: '/privacy',
terms: '/terms'
},
// Meta-Informationen für Navigation
meta: {
main: 'Hauptnavigation',
footer: 'Footer Navigation',
social: 'Social Media Navigation',
legal: 'Rechtliche Navigation'
}
} as const;
export type NavigationConfig = typeof navigation;
+33
View File
@@ -0,0 +1,33 @@
export const hero = {
// Navigation/Header
aboutMe: 'ÜBER MICH',
// Main Section
title: 'Digital Solutions Experte',
description: 'Mit einem Hintergrund in Softwareentwicklung und Expertise in digitaler Transformation helfe ich Unternehmen ihre Prozesse durch innovative Lösungen zu optimieren und zu automatisieren.',
// Expertise Areas
expertise: {
processAutomation: {
title: 'Prozessautomatisierung',
description: 'Entwicklung effizienter automatisierter Workflows'
},
systemIntegration: {
title: 'Systemintegration',
description: 'Verbindung und Optimierung von Geschäftssystemen'
},
customDevelopment: {
title: 'Individuelle Entwicklung',
description: 'Erstellung maßgeschneiderter Softwarelösungen'
}
},
// Languages
languages: {
english: 'Englisch',
serbian: 'Serbisch',
german: 'Deutsch',
french: 'Französisch',
spanish: 'Spanisch',
russian: 'Russisch'
}
} as const;
// Type für Typsicherheit
export type HeroTranslations = typeof hero;
+24
View File
@@ -0,0 +1,24 @@
import { hero } from './hero';
import { journey } from './journey';
import { schema } from './schema';
import { skillbar } from './skillbar';
import { skillgroup } from './skillgroup';
import { skills } from './skills';
import { workflow } from './workflow';
export const about = {
hero,
journey,
schema,
skillbar,
skillgroup,
skills,
workflow
} as const;
// Typen exportieren
export type { HeroTranslations } from './hero';
export type { JourneyTranslations } from './journey';
export type { SchemaTranslations } from './schema';
export type { SkillBarTranslations } from './skillbar';
export type { SkillGroupTranslations } from './skillgroup';
export type { SkillsTranslations } from './skills';
export type { WorkflowTranslations } from './workflow';
@@ -0,0 +1,95 @@
export const journey = {
// Header
title: 'Beruflicher Werdegang',
subtitle: 'Von digitalen Medien zu Unternehmenslösungen',
// Education Section
education: {
title: 'Ausbildung',
degrees: {
masters: {
degree: 'M.A. Softwareentwicklung',
university: 'Middlesex University, London',
period: '2020 - 2022'
},
bachelors: {
degree: 'B.A. Cross-Media Produktion',
university: 'Middlesex University, London',
period: '2018 - 2020'
}
}
},
// Languages Section
languages: {
title: 'Sprachen',
items: {
english: {
name: 'Englisch',
level: 'C2'
},
serbian: {
name: 'Serbisch',
level: 'C2'
},
german: {
name: 'Deutsch',
level: 'C2'
},
french: {
name: 'Französisch',
level: 'B2'
},
spanish: {
name: 'Spanisch',
level: 'B2'
},
russian: {
name: 'Russisch',
level: 'A1'
}
}
},
// Work History
work: {
positions: [
{
period: '08/2023 - Heute',
company: 'Ritter Digital GmbH',
title: 'Digital Solutions Consultant',
location: 'Oberhausen',
highlights: [
'ERP-Beratung und -Implementierung (JTL WaWi)',
'Prozessautomatisierung & Full-Stack-Entwicklung',
'RFID-Technologie & Automatisierung',
'Lead-Generierung & -Management',
'KI-gestütztes Content-Management'
]
},
{
period: '02/2023 - 08/2023',
company: 'Joyce & Girls Co AG',
title: 'ERP Integration Specialist',
location: 'Köln',
highlights: [
'Komplexe Systemintegration (Apparel Magic, TradeByte)',
'Server-Administration und Automatisierung',
'Datenbank- und API-Entwicklung',
'Digitales Marketing und Kampagnenanalyse'
]
},
{
period: '08/2022 - 01/2023',
company: 'Magnarius',
title: 'RPA Developer & E-Commerce Manager',
location: 'Bergisch Gladbach',
highlights: [
'Shopware 6 Entwicklung und JTL-Integration',
'Google Ads Automatisierung',
'CMS-Management und -Optimierung',
'SEO-Strategieumsetzung'
]
}
]
}
} as const;
// Type für Typsicherheit
export type JourneyTranslations = typeof journey;
+31
View File
@@ -0,0 +1,31 @@
export const schema = {
page: {
name: 'Über Damjan Savić',
description: 'Erfahren Sie mehr über Damjan Savić, einen zertifizierten JTL-Experten mit umfassender Erfahrung in E-Commerce und Warenwirtschaftslösungen.'
},
person: {
jobTitle: 'JTL Integrations-Experte',
description: 'Mit umfangreicher Erfahrung in der Integration und Optimierung von E-Commerce-Systemen unterstütze ich Unternehmen bei der digitalen Transformation ihrer Geschäftsprozesse.',
languages: {
english: 'Englisch',
serbian: 'Serbisch',
german: 'Deutsch',
french: 'Französisch',
spanish: 'Spanisch',
russian: 'Russisch'
},
skills: {
python: 'Python',
googleAds: 'Google Ads',
metaAds: 'Meta Ads',
shopware: 'Shopware',
shopify: 'Shopify',
wooCommerce: 'WooCommerce',
jtlWawi: 'JTL WAWI',
serverAdmin: 'Server-Administration',
airflow: 'AirFlow'
}
}
} as const;
// Type für Typsicherheit
export type SchemaTranslations = typeof schema;
@@ -0,0 +1,13 @@
export const skillbar = {
// Accessibility labels
ariaLabel: {
skillLevel: 'Fähigkeitsniveau',
progressBar: 'Fortschrittsbalken'
},
// Screen reader text
screenReader: {
progress: '{{level}} Prozent Fortschritt für {{skill}}'
}
} as const;
// Type für Typsicherheit
export type SkillBarTranslations = typeof skillbar;
@@ -0,0 +1,63 @@
export const skillgroup = {
// Kategorien
categories: {
development: 'Entwicklung',
ecommerce: 'E-Commerce',
marketing: 'Marketing',
automation: 'Automatisierung',
languages: 'Programmiersprachen',
databases: 'Datenbanken'
},
// Skills nach Kategorien
skills: {
development: {
frontend: 'Frontend-Entwicklung',
backend: 'Backend-Entwicklung',
fullstack: 'Full-Stack-Entwicklung',
api: 'API-Entwicklung',
testing: 'Software-Testing'
},
ecommerce: {
jtl: 'JTL WaWi',
shopware: 'Shopware',
shopify: 'Shopify',
woocommerce: 'WooCommerce',
magento: 'Magento'
},
marketing: {
seo: 'Suchmaschinenoptimierung',
sea: 'Suchmaschinenwerbung',
analytics: 'Web-Analyse',
googleAds: 'Google Ads',
metaAds: 'Meta Ads'
},
automation: {
rpa: 'Robotergestützte Prozessautomatisierung',
workflow: 'Workflow-Automatisierung',
cicd: 'CI/CD',
testing: 'Test-Automatisierung',
deployment: 'Deployment-Automatisierung'
},
languages: {
python: 'Python',
javascript: 'JavaScript',
typescript: 'TypeScript',
php: 'PHP',
sql: 'SQL'
},
databases: {
mysql: 'MySQL',
postgresql: 'PostgreSQL',
mongodb: 'MongoDB',
redis: 'Redis',
elasticsearch: 'Elasticsearch'
}
},
// Accessibility
ariaLabels: {
skillGroup: 'Fähigkeitsgruppe',
skillList: 'Liste der Fähigkeiten in dieser Kategorie'
}
} as const;
// Type für Typsicherheit
export type SkillGroupTranslations = typeof skillgroup;
+46
View File
@@ -0,0 +1,46 @@
// de/pages/about/skills.ts
export const skills = {
title: 'Fähigkeiten',
skillGroups: [
{
category: 'Entwicklung',
items: [
{ name: 'Python', level: 90 },
{ name: 'Google Ads', level: 85 },
{ name: 'Meta Ads', level: 85 }
]
},
{
category: 'E-Commerce',
items: [
{ name: 'Shopware', level: 85 },
{ name: 'Shopify', level: 85 },
{ name: 'WooCommerce', level: 85 }
]
},
{
category: 'Systemadministration',
items: [
{ name: 'JTL WAWI', level: 85 },
{ name: 'Server', level: 75 },
{ name: 'AirFlow', level: 60 }
]
}
],
ui: {
percentage: 'Prozent',
levelLabel: 'Niveau',
skillLevel: '{{level}}% Beherrschung in {{skill}}'
},
aria: {
skillGroup: 'Fähigkeitsgruppe für {{category}}',
skillBar: 'Fortschrittsbalken für {{skill}}',
skillIcon: 'Symbol für {{category}}',
selectSkill: 'Fähigkeit auswählen'
},
screenReader: {
skillProgress: '{{skill}} mit einem Niveau von {{level}} Prozent'
}
} as const;
export type SkillsTranslations = typeof skills;
@@ -0,0 +1,44 @@
export const workflow = {
// Seitentitel
title: 'MEIN WORKFLOW',
description: 'Systematischer Ansatz zur Softwareentwicklung durch definierte Schritte',
// Workflow-Schritte
steps: [
{
number: '01',
title: 'ANFORDERUNGSANALYSE'
},
{
number: '02',
title: 'ABFRAGE VON DEADLINES UND EINSCHRÄNKUNGEN'
},
{
number: '03',
title: 'EINRICHTUNG DER ENTWICKLUNGSUMGEBUNG'
},
{
number: '04',
title: 'PROGRAMMIERUNG'
},
{
number: '05',
title: 'TESTEN DER ERGEBNISSE'
},
{
number: '06',
title: 'SUPPORT UND SKALIERUNG DES CODES'
}
],
// Aria Labels für Barrierefreiheit
aria: {
workflowSection: 'Workflow-Prozess',
stepNumber: 'Schritt {{number}}',
stepDescription: 'Workflow-Schritt: {{title}}'
},
// Screen Reader Texte
screenReader: {
stepProgress: 'Schritt {{number}} von {{total}}: {{title}}'
}
} as const;
// Type für Typsicherheit
export type WorkflowTranslations = typeof workflow;
@@ -0,0 +1,39 @@
export const contactform = {
// Formular Header
title: 'Kontaktieren Sie mich',
description: 'Haben Sie Fragen oder möchten Sie zusammenarbeiten? Schreiben Sie mir eine Nachricht.',
// Formularfelder
name: {
label: 'Name',
placeholder: 'Ihr vollständiger Name'
},
email: {
label: 'E-Mail',
placeholder: 'ihre.email@beispiel.de'
},
message: {
label: 'Nachricht',
placeholder: 'Ihre Nachricht an mich...'
},
// Button
submit: 'Nachricht senden',
// Statusmeldungen
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. Bitte versuchen Sie es später erneut.',
// Fehlermeldungen
errors: {
nameRequired: 'Bitte geben Sie Ihren Namen ein',
emailRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein',
emailInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein',
messageRequired: 'Bitte geben Sie eine Nachricht ein'
},
// Aria Labels
aria: {
form: 'Kontaktformular',
submitting: 'Formular wird gesendet',
successAlert: 'Erfolgsmeldung',
errorAlert: 'Fehlermeldung'
}
} as const;
// Type für Typsicherheit
export type ContactFormTranslations = typeof contactform;
@@ -0,0 +1,25 @@
export const contactinfo = {
// Header
title: 'Kontaktinformationen',
description: 'Sie können mich über folgende Wege erreichen',
// Kontaktmethoden Labels
addressLabel: 'Bürostandort',
phoneLabel: 'Telefon',
emailLabel: 'E-Mail',
// Kontaktinformationen
address: 'Rotdornallee, Köln',
phone: '+49 175 695 0979',
email: 'info@damjan-savic.com',
// Zusätzliche Informationen
availabilityNote: 'Geschäftszeiten: Montag bis Freitag, 9:00 - 17:00 Uhr',
// Aria Labels
aria: {
contactCard: 'Kontaktinformationskarte',
addressLink: 'Öffne Standort in Google Maps',
phoneLink: 'Anrufen',
emailLink: 'E-Mail senden',
externalLink: 'Öffnet in neuem Tab'
}
} as const;
// Type für Typsicherheit
export type ContactInfoTranslations = typeof contactinfo;
@@ -0,0 +1,71 @@
// src/i18n/locales/de/pages/contact/index.ts
export const contact = {
breadcrumb: "KONTAKT",
hero: {
title: "Lassen Sie uns zusammenarbeiten",
subtitle: "Sie haben ein Projekt im Sinn? Lassen Sie uns besprechen, wie wir Ihnen bei der Erreichung Ihrer Ziele helfen können."
},
contactInfo: {
title: "Kontaktinformationen",
description: "Sie können mich über folgende Wege erreichen",
address: {
label: "Bürostandort",
value: "Rotdornallee, Köln"
},
phone: {
label: "Telefon",
value: "+49 175 695 0979"
},
email: {
label: "E-Mail",
value: "info@damjan-savic.com"
},
availabilityNote: "Geschäftszeiten: Montag bis Freitag, 9:00 - 17:00 Uhr",
aria: {
contactCard: "Kontaktinformationskarte",
addressLink: "Öffne Standort in Google Maps",
phoneLink: "Anrufen",
emailLink: "E-Mail senden",
externalLink: "Öffnet in neuem Tab"
}
},
contactForm: {
title: "Kontaktieren Sie mich",
description: "Haben Sie Fragen oder möchten Sie zusammenarbeiten? Schreiben Sie mir eine Nachricht.",
name: {
label: "Name",
placeholder: "Ihr vollständiger Name"
},
email: {
label: "E-Mail",
placeholder: "ihre.email@beispiel.de"
},
message: {
label: "Nachricht",
placeholder: "Ihre Nachricht an mich..."
},
submit: "Nachricht senden",
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. Bitte versuchen Sie es später erneut.",
errors: {
nameRequired: "Bitte geben Sie Ihren Namen ein",
emailRequired: "Bitte geben Sie Ihre E-Mail-Adresse ein",
emailInvalid: "Bitte geben Sie eine gültige E-Mail-Adresse ein",
messageRequired: "Bitte geben Sie eine Nachricht ein"
},
aria: {
form: "Kontaktformular",
submitting: "Formular wird gesendet",
successAlert: "Erfolgsmeldung",
errorAlert: "Fehlermeldung"
}
}
} as const;
export type ContactTranslations = typeof contact;
export default {
pages: {
contact: contact
}
};
+76
View File
@@ -0,0 +1,76 @@
// src/i18n/locales/de/pages/dashboard.ts
export const dashboard = {
// SEO
seo: {
title: 'Analytics Dashboard',
description: 'Website-Analysedashboard'
},
// Hauptüberschriften
header: {
title: 'Analytics Dashboard',
activeUsers: 'aktive Nutzer'
},
// Metrikkarten
metrics: {
pageViews: {
title: 'Seitenaufrufe',
perMinute: 'pro Minute'
},
uniqueVisitors: {
title: 'Eindeutige Besucher',
currentlyActive: 'aktuell aktiv'
},
conversions: {
title: 'Konversionen',
rate: 'Konversionsrate'
},
timeOnSite: {
title: 'Durchschn. Besuchszeit',
bounceRate: 'Absprungrate'
}
},
// Top-Seiten Bereich
topPages: {
title: 'Top-Seiten',
columns: {
path: 'Pfad',
views: 'Aufrufe',
change: 'Änderung'
}
},
// Event-Tracking Bereich
events: {
title: 'Event-Tracking',
types: {
contactForm: 'Kontaktformular Absendung',
portfolioView: 'Portfolio Ansicht',
cvDownload: 'Lebenslauf Download',
blogRead: 'Blog Gelesen'
}
},
// Statusmeldungen
status: {
loading: 'Lade Dashboard...',
error: 'Fehler beim Laden der Daten',
notAuthenticated: 'Bitte melden Sie sich an'
},
// Sonstiges
misc: {
viewMore: 'Mehr anzeigen',
refresh: 'Aktualisieren',
period: {
today: 'Heute',
yesterday: 'Gestern',
last7Days: 'Letzte 7 Tage',
last30Days: 'Letzte 30 Tage',
thisMonth: 'Dieser Monat',
lastMonth: 'Letzter Monat'
}
},
// Trends und Änderungen
trends: {
increase: 'Zunahme',
decrease: 'Abnahme',
noChange: 'Keine Änderung'
}
};
+9
View File
@@ -0,0 +1,9 @@
// src/i18n/locales/de/pages/home/about.ts
export const about = {
title: 'Über Mich',
content: 'Als Berater im Bereich der Digitalisierung helfe ich Unternehmen, ihre Abläufe durch moderne Software zu verbessern. Ich entwickle Lösungen zur Verbindung von Warenwirtschaftssystemen, Programme zur Automatisierung von Arbeitsabläufen und RFID-Anwendungen zur besseren Warenverfolgung. Bei der Ritter Digital GmbH arbeite ich mit JTL-Software und entwickle individuelle Programmlösungen. Ein weiteres Projekt war die Verbindung der Systeme von Breuninger mit Apparel Magic über TB.One. Mein Ziel ist es, Unternehmen durch Software effizienter zu machen.',
image: {
alt: 'Portrait von Damjan Savić'
}
} as const;
export type AboutTranslations = typeof about;
@@ -0,0 +1,77 @@
// src/i18n/locales/de/pages/home/experience.ts
export const experience = {
title: 'Berufserfahrung',
navigation: {
prev: '←',
next: '→'
},
positions: [
{
role: 'Consultant Digital Solutions & Developer',
company: 'RITTER Gesellschaft für digitale Geschäftsprozesse mbH',
period: '08/2023 - HEUTE',
highlights: [
'JTL WaWi, WMS und POS Beratung & Implementierung',
'Python-basierte Automationslösungen',
'RFID-Systeme und GUI-Entwicklung',
'KI & Business Intelligence'
]
},
{
role: 'ERP-Integrationsspezialist & E-Commerce-Entwickler',
company: 'Joyce & Girls',
period: '01/2023 - 08/2023',
highlights: [
'ApparelMagic & TradeByte Integration',
'MariaDB Middleware-Entwicklung',
'Windows Server Administration',
'Shopify-Optimierung'
]
},
{
role: 'RPA Developer & E-Commerce Manager',
company: 'C&S Marketing',
period: '08/2022 - 01/2023',
highlights: [
'Shopware 6 & JTL Integration',
'Python Automatisierung',
'Google Ads Optimierung',
'CMS Management'
]
},
{
role: 'E-Commerce Developer',
company: 'Brands Club GmbH',
period: '08/2021 - 07/2022',
highlights: [
'Shopify & JTL Integration',
'Server Administration',
'Content Production',
'Marketing Automation'
]
},
{
role: 'E-Commerce Developer',
company: 'Feine Uhren Eupen',
period: '01/2021 - 08/2021',
highlights: [
'Shopware 5 Development',
'Produktfotografie',
'Sales im Luxusuhrensegment',
'E-Commerce Management'
]
},
{
role: 'Online Marketing Manager',
company: 'Ufer8, Ambis, Teatro & Die Halle Tor 2',
period: '01/2018 - 12/2020',
highlights: [
'Social Media Content',
'Event Marketing',
'Veranstaltungsorganisation',
'Marketing Automation'
]
}
]
} as const;
export type ExperienceTranslations = typeof experience;
@@ -0,0 +1,27 @@
// src/i18n/locales/de/pages/home/expertise.ts
export const expertise = {
title: 'Bereiche meiner Expertise',
areas: [
{
title: 'ERP Integration',
description: 'Expert für JTL-Wawi Implementierung, Konfiguration und Optimierung. Spezialisiert auf Warenwirtschaftssysteme und POS-Integration.',
skills: ['JTL-Wawi', 'JTL-WMS', 'JTL-POS', 'Apparel Magic']
},
{
title: 'Entwicklung',
description: 'Full-Stack Entwicklung mit Fokus auf Python-Automatisierung, React Frontends und Datenbankdesign.',
skills: ['Python', 'React/Next.js', 'MariaDB', 'Docker']
},
{
title: 'E-Commerce',
description: 'Umfangreiche Erfahrung mit verschiedenen E-Commerce-Plattformen und Marktplatz-Integrationen.',
skills: ['Shopify', 'Shopware', 'WooCommerce', 'API Integration']
},
{
title: 'Digitales Marketing',
description: 'Strategischer Ansatz im digitalen Marketing mit Fokus auf Automatisierung und Analyse.',
skills: ['Google Ads', 'Meta Ads', 'SEO', 'Analytics']
}
]
} as const;
export type ExpertiseTranslations = typeof expertise;
+29
View File
@@ -0,0 +1,29 @@
// src/i18n/locales/de/pages/home/hero.ts
export const hero = {
title: 'SOFTWARE ENGINEER',
name: 'DAMJAN SAVIĆ',
cta: 'KONTAKT AUFNEHMEN',
image: {
alt: 'Portrait von Damjan Savić'
},
social: {
getInTouch: 'Kontaktieren Sie mich',
linkedin: {
title: 'Besuchen Sie mein LinkedIn Profil'
},
github: {
title: 'Besuchen Sie mein GitHub Profil'
},
email: {
title: 'Senden Sie mir eine E-Mail'
}
},
navigation: {
experience: 'ERFAHRUNG',
skills: 'FÄHIGKEITEN',
projects: 'PROJEKTE',
about: 'ÜBER MICH'
}
} as const;
export type HeroTranslations = typeof hero;
+18
View File
@@ -0,0 +1,18 @@
// src/i18n/locales/de/pages/home/index.ts
import { hero } from './hero';
import { about } from './about';
import { experience } from './experience';
import { expertise } from './expertise';
import { skills } from './skills';
import { projects } from './projects';
export const home = {
hero,
about,
experience,
expertise,
skills,
projects
} as const;
export type HomeTranslations = typeof home;
@@ -0,0 +1,10 @@
// src/i18n/locales/de/pages/home/projects.ts
export const projects = {
title: 'Portfolio',
viewAll: 'Alle Projekte →',
loading: 'Projekte werden geladen...',
error: {
loading: 'Fehler beim Laden der Projekte'
}
} as const;
export type ProjectsTranslations = typeof projects;
+41
View File
@@ -0,0 +1,41 @@
// src/i18n/locales/de/pages/home/skills.ts
export const skills = {
title: 'Fähigkeiten',
skills: [
{
name: 'ERP',
level: 90,
description: 'Enterprise Resource Planning'
},
{
name: 'Datenbank',
level: 85,
description: 'Datenbankentwicklung und -administration'
},
{
name: 'E-Commerce',
level: 95,
description: 'Online-Handel und Plattformen'
},
{
name: 'Python',
level: 80,
description: 'Programmiersprache Python'
},
{
name: 'React',
level: 75,
description: 'Frontend-Entwicklung mit React'
},
{
name: 'Server',
level: 70,
description: 'Server-Administration'
}
],
aria: {
skillLevel: 'Fähigkeitsniveau',
selectSkill: 'Fähigkeit auswählen'
}
} as const;
export type SkillsTranslations = typeof skills;
+54
View File
@@ -0,0 +1,54 @@
// src/i18n/locales/de/pages/login.ts
export const login = {
// SEO
seo: {
title: 'Login',
description: 'Anmelden für Zugriff auf das Dashboard'
},
// Hauptüberschrift
header: {
title: 'Im Dashboard anmelden'
},
// Formular
form: {
// Felder
email: {
label: 'E-Mail-Adresse',
placeholder: 'ihre.email@beispiel.de'
},
password: {
label: 'Passwort',
placeholder: 'Ihr Passwort'
},
// Buttons
submit: {
default: 'Anmelden',
loading: 'Anmeldung läuft...'
},
// Fehlermeldungen
errors: {
default: 'Ein Fehler ist aufgetreten',
invalidCredentials: 'Ungültige Anmeldedaten',
emailRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein',
passwordRequired: 'Bitte geben Sie Ihr Passwort ein',
emailInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein'
}
},
// Links und Hilfe
help: {
forgotPassword: 'Passwort vergessen?',
needHelp: 'Benötigen Sie Hilfe?',
contactSupport: 'Support kontaktieren'
},
// Statusmeldungen
status: {
authenticating: 'Authentifizierung...',
success: 'Erfolgreich angemeldet',
redirecting: 'Weiterleitung zum Dashboard...'
},
// Sicherheitshinweise
security: {
secureConnection: 'Sichere Verbindung',
privacyNote: 'Ihre Daten werden verschlüsselt übertragen'
}
};
+19
View File
@@ -0,0 +1,19 @@
// src/i18n/locales/de/pages/notfound.ts
export const notfound = {
// SEO
seo: {
title: '404 - Seite nicht gefunden',
description: 'Die gesuchte Seite existiert nicht.'
},
// Hauptinhalt
content: {
errorCode: '404',
title: 'Seite nicht gefunden',
description: 'Die gesuchte Seite existiert nicht oder wurde verschoben.',
},
// Aktionsbuttons
actions: {
goBack: 'Zurück',
homePage: 'Startseite'
}
};
+111
View File
@@ -0,0 +1,111 @@
// src/i18n/locales/de/pages/portfolio.ts
export const portfolio = {
// SEO und Meta-Informationen
seo: {
title: 'Portfolio - Damjan Savić',
description: 'Entdecken Sie meine Projekte im Bereich JTL-Integration, E-Commerce und Digitale Transformation.'
},
// Hauptseite
main: {
title: 'Portfolio',
loading: 'Projekte werden geladen...',
error: 'Fehler beim Laden der Projekte',
noProjects: {
title: 'Keine Projekte gefunden',
description: 'Aktuell sind keine Projekte verfügbar. Schauen Sie später wieder vorbei.'
}
},
// Projekt-Karten
projectCard: {
date: 'Datum',
technologies: 'Technologien',
moreCount: 'weitere', // für '+X weitere'
readMore: 'Mehr erfahren'
},
// Projekt-Details
project: {
navigation: {
back: 'Zurück zum Portfolio',
projectDetails: 'PROJEKT DETAILS'
},
metadata: {
date: 'Datum',
client: 'Kunde',
duration: 'Dauer',
repository: 'Repository',
documentation: 'Dokumentation',
technologies: 'Technologien',
live: 'Live-Version'
},
sections: {
overview: 'Überblick',
challenge: 'Herausforderung',
solution: 'Lösung',
results: 'Ergebnisse',
systemArchitecture: 'Systemarchitektur',
implementation: 'Implementierung',
technologies: 'Verwendete Technologien'
}
},
// Kategorien und Filter
categories: {
all: 'Alle',
'Data Science': 'Data Science',
'AI Development': 'KI-Entwicklung',
'Integration': 'Integration',
'Full-Stack Development': 'Full-Stack Entwicklung',
'Data Processing': 'Datenverarbeitung',
'Content Production': 'Content-Produktion',
'Machine Learning': 'Machine Learning',
'Automation': 'Automatisierung'
},
// Spezifische Projekte
projects: {
'ai-data-reader': {
title: 'KI-basierte Produktdatenpflege aus PDF-Dateien',
description: 'Entwicklung eines automatisierten Systems zur Extraktion und Strukturierung von Produktdaten aus PDF-Dokumenten'
},
'bi-vision': {
title: 'KI-Videos für BI-Software',
description: 'Entwicklung von KI-gestützten Erklärvideos für Business Intelligence Dashboards'
},
'business-intelligence-ai': {
title: 'KI für Business Intelligence',
description: 'Integration von KI-Modellen in Business Intelligence Systeme zur Prozessoptimierung'
},
'claude-personal-assistant': {
title: 'KI-basierter Personal Assistant mit Claude AI',
description: 'Entwicklung eines personalisierten KI-Assistenten für professionelle E-Mail-Kommunikation'
},
'e-commerce-integration': {
title: 'Integration mit TradeByte',
description: 'Integration eines Shopify-Shops mit JTL-WaWi für optimierte Bestandsführung'
},
'jtl-integration-project': {
title: 'JTL Integration Referenzprojekt',
description: 'Ein vollständiges Lagersystem in Krefeld auf Basis von JTL'
},
'local-llm-training': {
title: 'Training von lokalen KI-Modellen',
description: 'Entwicklung einer Pipeline für das Training und die Feinabstimmung lokaler KI-Modelle'
},
'processautomation': {
title: 'Automatisierung mit Python',
description: 'Entwicklung von Automatisierungslösungen für Geschäftsprozesse'
},
'rfid-automation': {
title: 'RFID-Automatisierung',
description: 'Entwicklung eines automatisierten Systems zur Erstellung und zum Druck von RFID-Etiketten'
},
'timetracking-software': {
title: 'Digitale Zeiterfassung',
description: 'Entwicklung einer modernen Zeiterfassungslösung mit Next.js, Flask und MSSQL'
}
},
// Status-Meldungen
status: {
loading: 'Wird geladen...',
error: 'Ein Fehler ist aufgetreten',
success: 'Erfolgreich geladen'
}
};
+96
View File
@@ -0,0 +1,96 @@
// src/i18n/locales/de/pages/privacy.ts
export const privacy = {
// SEO und Schema
seo: {
title: 'Datenschutzerklärung',
description: 'Datenschutzerklärung für die Portfolio-Website von Damjan Savić'
},
schema: {
name: 'Datenschutzerklärung',
description: 'Datenschutzerklärung für die Portfolio-Website von Damjan Savić'
},
// Hauptinhalt
content: {
title: 'Datenschutzerklärung',
lastUpdated: 'Zuletzt aktualisiert: 10. März 2024',
// Navigation
breadcrumbs: {
currentTitle: 'Datenschutzerklärung'
},
// Abschnitte
sections: {
introduction: {
title: '1. Einführung',
content: 'Diese Datenschutzerklärung erläutert, wie wir Ihre personenbezogenen Daten bei der Nutzung unserer Website erfassen, verwenden und schützen.'
},
dataCollection: {
title: '2. Datenerfassung',
providedInfo: {
title: '2.1 Von Ihnen bereitgestellte Informationen',
intro: 'Bei Verwendung unseres Kontaktformulars erfassen wir:',
items: [
'Name',
'E-Mail-Adresse',
'Nachrichteninhalt'
]
},
automaticInfo: {
title: '2.2 Automatisch erfasste Informationen',
intro: 'Wir erfassen automatisch:',
items: [
'IP-Adresse',
'Browser-Typ',
'Geräteinformationen',
'Besuchte Seiten',
'Verweildauer auf der Website'
]
}
},
dataUsage: {
title: '3. Verwendung Ihrer Daten',
intro: 'Wir verwenden Ihre Daten für:',
items: [
'Beantwortung Ihrer Anfragen',
'Verbesserung unserer Website',
'Analyse von Nutzungsmustern',
'Betrugsprävention'
]
},
cookies: {
title: '4. Cookies',
intro: 'Wir verwenden Cookies für:',
items: [
'Speicherung Ihrer Präferenzen',
'Analyse des Website-Verkehrs',
'Verständnis des Nutzerverhaltens'
]
},
security: {
title: '5. Datensicherheit',
intro: 'Wir implementieren geeignete Sicherheitsmaßnahmen zum Schutz Ihrer personenbezogenen Daten, einschließlich:',
items: [
'Verschlüsselung der Datenübertragung',
'Regelmäßige Sicherheitsbewertungen',
'Zugriffskontrollen',
'Sichere Datenspeicherung'
]
},
rights: {
title: '6. Ihre Rechte',
intro: 'Sie haben das Recht auf:',
items: [
'Zugriff auf Ihre personenbezogenen Daten',
'Berichtigung der Daten',
'Löschung der Daten',
'Widerspruch gegen die Datenverarbeitung',
'Datenübertragbarkeit'
]
},
contact: {
title: '7. Kontaktinformationen',
intro: 'Für datenschutzbezogene Anfragen kontaktieren Sie uns unter:',
email: 'E-Mail: privacy@damjan-savic.com'
}
}
}
};
+59
View File
@@ -0,0 +1,59 @@
// src/i18n/locales/de/pages/terms.ts
export const terms = {
// SEO und Schema
seo: {
title: 'Allgemeine Geschäftsbedingungen',
description: 'Allgemeine Geschäftsbedingungen für die Portfolio-Website von Damjan Savić'
},
schema: {
name: 'Allgemeine Geschäftsbedingungen',
description: 'Allgemeine Geschäftsbedingungen für die Portfolio-Website von Damjan Savić'
},
// Hauptinhalt
content: {
title: 'Allgemeine Geschäftsbedingungen',
lastUpdated: 'Zuletzt aktualisiert: 10. März 2024',
// Navigation
breadcrumbs: {
currentTitle: 'Allgemeine Geschäftsbedingungen'
},
// Abschnitte
sections: {
agreement: {
title: '1. Zustimmung zu den Bedingungen',
content: 'Mit dem Zugriff auf unsere Website stimmen Sie zu, an diese Allgemeinen Geschäftsbedingungen gebunden zu sein und alle geltenden Gesetze und Vorschriften einzuhalten.'
},
intellectualProperty: {
title: '2. Rechte an geistigem Eigentum',
content: 'Alle Inhalte dieser Website, einschließlich, aber nicht beschränkt auf Texte, Grafiken, Logos, Bilder und Software, sind Eigentum von Damjan Savić und durch Gesetze zum Schutz geistigen Eigentums geschützt.'
},
userResponsibilities: {
title: '3. Nutzerverantwortlichkeiten',
intro: 'Bei der Nutzung unserer Website stimmen Sie zu:',
items: [
'Korrekte Informationen bereitzustellen',
'Die Website legal und ethisch zu nutzen',
'Keinen unbefugten Zugriff zu versuchen',
'Den Betrieb der Website nicht zu stören'
]
},
liability: {
title: '4. Haftungsbeschränkung',
content: 'Wir haften nicht für indirekte, zufällige, besondere, Folge- oder Strafschäden, die sich aus Ihrer Nutzung oder Unfähigkeit zur Nutzung der Website ergeben.'
},
changes: {
title: '5. Änderungen der Bedingungen',
content: 'Wir behalten uns das Recht vor, diese Bedingungen jederzeit zu ändern. Wir werden die Nutzer über wesentliche Änderungen informieren, indem wir die neuen Allgemeinen Geschäftsbedingungen auf dieser Seite veröffentlichen.'
},
governingLaw: {
title: '6. Geltendes Recht',
content: 'Diese Bedingungen unterliegen den Gesetzen der Bundesrepublik Deutschland und werden in Übereinstimmung mit diesen ausgelegt, ohne Berücksichtigung von Kollisionsnormen.'
},
contact: {
title: '7. Kontaktinformationen',
intro: 'Bei Fragen zu diesen Allgemeinen Geschäftsbedingungen kontaktieren Sie uns bitte unter:',
email: 'E-Mail: legal@damjan-savic.com'
}
}
}
};
+53
View File
@@ -0,0 +1,53 @@
// src/i18n/locales/de/portfolio/index.ts
import aiDataReader from './projects/ai-data-reader';
export const portfolio = {
// Meta-Informationen für die Portfolio-Seite
seo: {
title: 'Portfolio - Damjan Savić',
description: 'Entdecken Sie meine Projekte im Bereich JTL-Integration, E-Commerce und digitale Transformation.'
},
// Portfolio-Überschriften und Filter
sections: {
title: 'Portfolio',
subtitle: 'Ausgewählte Projekte und Referenzen',
filterTitle: 'Kategorien',
sortTitle: 'Sortierung'
},
// Filter-Optionen
filters: {
all: 'Alle',
categories: {
'Data Science': 'Data Science',
'AI Development': 'KI-Entwicklung',
'Integration': 'Integration',
'Full-Stack Development': 'Full-Stack Entwicklung',
'Data Processing': 'Datenverarbeitung',
'Content Production': 'Content-Produktion',
'Machine Learning': 'Machine Learning',
'Automation': 'Automatisierung'
}
},
// Sortier-Optionen
sort: {
date: 'Datum',
title: 'Titel',
category: 'Kategorie'
},
// UI-Texte
ui: {
loading: 'Projekte werden geladen...',
noProjects: 'Keine Projekte gefunden',
viewProject: 'Projekt ansehen',
backToPortfolio: 'Zurück zum Portfolio',
technologies: 'Technologien',
client: 'Kunde',
duration: 'Dauer',
visitWebsite: 'Website besuchen',
viewRepository: 'Repository ansehen',
viewDocumentation: 'Dokumentation'
},
// Alle Projekte
projects: {
'ai-data-reader': aiDataReader,
}
};
@@ -0,0 +1,107 @@
// src/i18n/locales/de/portfolio/projects/ai-data-reader.ts
export const aiDataReader = {
meta: {
slug: 'ai-data-reader',
title: "KI-basierte Produktdatenpflege aus PDF-Dateien",
description: "Entwicklung eines automatisierten Systems zur Extraktion und Strukturierung von Produktdaten aus PDF-Dokumenten",
excerpt: "Integration von Claude AI für die intelligente Verarbeitung von Produktinformationen und nahtlose JTL-ERP-Integration.",
date: "2024-02",
category: "Data Processing",
client: "Amazon Seller",
duration: "2 Monate",
url: "https://www.damjan-savic.com",
repository: "",
documentation: "",
published: true,
featured: true,
technologies: ["Python", "FastAPI", "Claude AI", "PyPDF", "Pydantic", "JTL-Wawi"],
tags: ["Data Extraction", "ERP", "Process Automation", "PDF Processing"]
},
content: {
intro: "Ein KI-gestütztes System zur automatisierten Extraktion von Produktdaten aus PDF-Dokumenten mit direkter Integration in JTL-Warenwirtschaftssysteme, entwickelt für maximale Effizienz in der Produktdatenpflege.",
challenge: {
title: "Die Herausforderung",
description: "Die manuelle Erfassung von Produktdaten aus PDF-Dokumenten war zeitaufwendig und fehleranfällig.",
points: [
"Große Mengen an PDF-Dokumenten mit Produktinformationen",
"Zeitintensive manuelle Dateneingabe",
"Inkonsistente Datenqualität",
"Hoher personeller Aufwand"
]
},
solution: {
title: "Unsere Lösung",
description: "Entwicklung eines KI-gestützten Systems zur automatisierten Datenextraktion",
content: "Durch den Einsatz von Claude AI und modernen Python-Technologien haben wir eine vollautomatische Lösung geschaffen, die PDF-Dokumente verarbeitet und die extrahierten Daten direkt in das JTL-Warenwirtschaftssystem integriert.",
points: [
"Automatisierte PDF-Verarbeitung",
"KI-gestützte Datenextraktion",
"Intelligente Datenvalidierung",
"Nahtlose JTL-Integration"
]
},
technical: {
title: "Technische Umsetzung",
description: "Die Lösung basiert auf einer modularen Python-Architektur mit verschiedenen Kernkomponenten:",
points: [
"Robuste PDF-Textextraktion mit PyPDF",
"KI-gestützte Datenextraktion mit Claude AI",
"Validierung durch Pydantic Models",
"JTL-ERP Integration über CSV-Export"
],
code: `class ProductData(BaseModel):
artikel_nummer: str
name: str
hersteller: str
inhaltsstoffe: List[str]
allergene: List[str]
naehrwerte: NaehrwertData
nettogewicht: str
vegan: bool
vegetarisch: bool
def _extract_with_claude(self, prompt: str) -> Dict[str, Any]:
'''Extrahiert strukturierte Daten mit Claude AI.'''
response = self.client.messages.create(
model='claude-3-sonnet-20240229',
messages=[{
'role': 'user',
'content': prompt
}]
)
return self._clean_json_string(response.content)`
},
implementation: {
title: "Implementierungsprozess",
description: "Der automatisierte Workflow wurde in mehreren Phasen implementiert:",
points: [
"PDF-Batch-Verarbeitung aus Input-Verzeichnis",
"Intelligente Texterkennung und -strukturierung",
"Mehrschichtige Validierungslogik",
"Robuste Fehlerbehandlung",
"Integration in bestehende Systeme"
]
},
results: {
title: "Ergebnisse und Auswirkungen",
description: "Die Implementierung führte zu signifikanten Verbesserungen im Geschäftsprozess:",
points: [
"90% Zeitersparnis bei der Produktdatenpflege",
"99% Genauigkeit bei der Datenextraktion",
"Eliminierung manueller Dateneingaben",
"Standardisierte Produktdatenqualität",
"Deutlich reduzierte Fehlerquote"
]
},
conclusion: "Das entwickelte System hat die Produktdatenpflege grundlegend revolutioniert. Durch die Kombination von KI-Technologie und automatisierter Datenverarbeitung wurde nicht nur die Effizienz drastisch gesteigert, sondern auch die Datenqualität erheblich verbessert. Das Projekt zeigt eindrucksvoll, wie moderne KI-Lösungen praktische Geschäftsprobleme lösen können."
}
};
// Re-export für Kompatibilität mit dem Projektimport-System
export default aiDataReader;
+175
View File
@@ -0,0 +1,175 @@
// src/i18n/locales/en/blog/index.ts
export const blog = {
meta: {
title: 'Blog - JTL Integration & Digital Solutions',
description: 'Insights and Best Practices on JTL-WaWi, E-Commerce, and Digital Transformation',
header: {
title: 'Insights & Best Practices',
subtitle: 'Insights and Best Practices on JTL-WaWi, E-Commerce, and Digital Transformation'
}
},
posts: {
'erp-integration-breuninger': {
title: 'ApparelMagic and TradeByte: Analysis of Complex Integrations',
date: '2024-02-09',
excerpt: 'Detailed analysis of an e-commerce integration between Apparel Magic and Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
content: {
intro: {
title: 'Apparel Magic meets Breuninger: A Technical Analysis of the TradeByte Integration',
description: 'The integration of e-commerce systems with established marketplaces poses unique challenges for companies. In this article, I share my experiences from a complex integration project between Apparel Magic as an ERP system and the Breuninger platform via TradeByte.'
},
background: {
title: 'Project Background',
systems: {
erp: 'Apparel Magic as an ERP system for product data and inventory management',
middleware: 'TradeByte as the middleware for the Breuninger platform'
},
challenges: {
sync: 'Bidirectional synchronization of orders',
customers: 'Automated customer creation',
delivery: 'Management of delivery notes',
inventory: 'Real-time inventory management'
}
},
tech: {
title: 'Technical Architecture',
components: {
title: 'Key Components'
},
database: {
title: 'Middleware Database',
description: 'The MariaDB database serves as the central data store for the integration:'
}
},
api: {
title: 'API Integrations',
apparel_magic: {
title: 'Apparel Magic API',
description: 'The REST API of Apparel Magic is used for customer creation and inventory management:'
},
tradebyte: {
title: 'TradeByte Integration',
description: 'The TradeByte API uses a combination of REST and XML:'
}
},
order_process: {
title: 'Order Process',
steps: {
fetch: 'Regular retrieval of new orders from TradeByte',
customers: 'Automatic customer creation in Apparel Magic',
process: 'Order processing and status update',
delivery: 'Generation and storage of delivery notes'
},
delivery_notes: {
title: 'Delivery Note Management'
}
},
challenges: {
title: 'Challenges and Solutions',
status: {
title: '1. Status Management',
description: 'A particular challenge was managing order statuses:'
},
error_handling: {
title: '2. Error Handling and Monitoring',
description: 'Robust error handling was essential for production operation:'
}
},
best_practices: {
title: 'Best Practices',
api: {
title: '1. API Communication',
items: {
retry: 'Implementation of retry mechanisms',
errors: 'Careful error handling',
logging: 'Comprehensive logging of all communications'
}
},
data: {
title: '2. Data Management',
items: {
storage: 'Central data storage in MariaDB',
transactions: 'Transactional security for critical operations',
validation: 'Regular data validation'
}
},
automation: {
title: '3. Process Automation',
items: {
customers: 'Automated customer creation',
status: 'Automatic status updates',
delivery: 'Systematic delivery note management'
}
}
},
lessons: {
title: 'Lessons Learned',
api: {
title: '1. API Design',
items: {
docs: 'Thorough API documentation is essential',
auth: 'Understanding of various authentication methods',
limits: 'Consideration of rate limits'
}
},
validation: {
title: '2. Data Validation',
items: {
rules: 'Implementation of strict validation rules',
edge_cases: 'Careful handling of edge cases',
cleanup: 'Automated data cleanup'
}
},
optimization: {
title: '3. Process Optimization',
items: {
automation: 'Continuous improvement of automation',
performance: 'Regular performance reviews',
monitoring: 'Proactive monitoring'
}
}
},
conclusion: {
title: 'Conclusion',
requirements: {
understanding: 'Deep understanding of both systems',
implementation: 'Careful implementation of API communication',
error_handling: 'Robust error handling',
monitoring: 'Continuous monitoring'
},
results: 'The developed solution successfully processes orders and enables seamless integration between the systems. In particular, automated customer creation and delivery note management have proven to be efficiency boosters.'
}
}
},
},
ui: {
readMore: 'Read More',
loading: {
posts: 'Loading blog posts...',
post: 'Loading blog post...'
},
errors: {
posts: 'Error loading blog posts',
post: 'The requested blog post does not exist or has been moved.'
},
backToBlog: 'Back to Blog',
publishedOn: 'Published on',
category: 'Category',
tags: 'Tags',
notFound: {
title: 'Post Not Found',
message: 'The requested blog post does not exist or has been moved.'
}
},
categories: {
'Integration': 'Integration',
'Entwicklung': 'Development',
'Automatisierung': 'Automation'
}
} as const;
export type BlogConfig = typeof blog;
export type BlogPost = BlogConfig['posts'][keyof BlogConfig['posts']];
@@ -0,0 +1,138 @@
// src/i18n/locales/en/blog/posts/erp-integration-breuninger.ts
export const erpIntegrationBreuninger = {
posts: {
erp_integration: {
title: "ApparelMagic and TradeByte: Analysis of Complex Integrations",
date: "2024-02-09",
excerpt: "Detailed analysis of an e-commerce integration between Apparel Magic and Breuninger via TradeByte",
category: "System Integration",
tags: ["ERP", "Apparel Magic", "TradeByte", "API Integration", "Python", "MariaDB"],
coverImage: "/images/posts/erp-integration-breuninger/cover.jpg",
intro: {
title: "Apparel Magic meets Breuninger: A Technical Analysis of the TradeByte Integration",
description: "The integration of e-commerce systems with established marketplaces presents unique challenges for companies. In this article, I share my experiences from a complex integration project between Apparel Magic as an ERP system and the Breuninger platform via TradeByte."
},
background: {
title: "Project Background",
systems: {
erp: "Apparel Magic as an ERP system for product data and inventory management",
middleware: "TradeByte as middleware for the Breuninger platform"
},
challenges: {
sync: "Bidirectional synchronization of orders",
customers: "Automated customer creation",
delivery: "Management of delivery notes",
inventory: "Real-time inventory management"
}
},
tech: {
title: "Technical Architecture",
components: {
title: "Central Components"
},
database: {
title: "Middleware Database",
description: "The MariaDB database serves as the central data store for the integration:"
}
},
api: {
title: "API Integrations",
apparel_magic: {
title: "Apparel Magic API",
description: "The REST API of Apparel Magic is used for customer creation and inventory management:"
},
tradebyte: {
title: "TradeByte Integration",
description: "The TradeByte API uses a combination of REST and XML:"
}
},
order_process: {
title: "Order Process",
steps: {
fetch: "Regular retrieval of new orders from TradeByte",
customers: "Automatic customer creation in Apparel Magic",
process: "Order processing and status update",
delivery: "Generation and storage of delivery notes"
},
delivery_notes: {
title: "Delivery Note Management"
}
},
challenges: {
title: "Challenges and Solutions",
status: {
title: "1. Status Management",
description: "A particular challenge was managing order statuses:"
},
error_handling: {
title: "2. Error Handling and Monitoring",
description: "Robust error handling was essential for production operations:"
}
},
best_practices: {
title: "Best Practices",
api: {
title: "1. API Communication",
items: {
retry: "Implementation of retry mechanisms",
errors: "Careful error handling",
logging: "Comprehensive logging of all communications"
}
},
data: {
title: "2. Data Management",
items: {
storage: "Central data storage in MariaDB",
transactions: "Transactional security for critical operations",
validation: "Regular data validation"
}
},
automation: {
title: "3. Process Automation",
items: {
customers: "Automated customer creation",
status: "Automatic status updates",
delivery: "Systematic delivery note management"
}
}
},
lessons: {
title: "Lessons Learned",
api: {
title: "1. API Design",
items: {
docs: "Thorough API documentation is essential",
auth: "Understanding of various authentication methods",
limits: "Consideration of rate limits"
}
},
validation: {
title: "2. Data Validation",
items: {
rules: "Implementation of strict validation rules",
edge_cases: "Careful handling of edge cases",
cleanup: "Automated data cleanup"
}
},
optimization: {
title: "3. Process Optimization",
items: {
automation: "Continuous improvement of automation",
performance: "Regular performance reviews",
monitoring: "Proactive monitoring"
}
}
},
conclusion: {
title: "Conclusion",
requirements: {
understanding: "Deep understanding of both systems",
implementation: "Careful implementation of the API communication",
error_handling: "Robust error handling",
monitoring: "Continuous monitoring"
},
results: "The developed solution successfully processes orders and enables seamless integration between the systems. In particular, the automated customer creation and delivery note management have proven to enhance efficiency."
}
}
}
};
@@ -0,0 +1,253 @@
// src/i18n/locales/en/blog/posts/fullstack-development-timetracking.ts
export const fullstackDevelopmentTimetracking = {
meta: {
title: 'Fullstack Development with Python and React: Architecture of our Time Tracking Solution',
date: '2024-02-09',
excerpt: 'A technical deep dive into the implementation of a modern time tracking solution',
category: 'System Architecture',
coverImage: '/images/posts/fullstack-development-timetracking/cover.jpg',
tags: ['Python', 'React', 'TypeScript', 'MSSQL', 'System Design']
},
content: {
intro: {
title: 'Fullstack Development with Python and React: Architecture of our Time Tracking Solution',
description: 'Developing a robust time tracking solution requires not only technical expertise but also a deep understanding of complex business rules and user requirements. In this article, I share our experiences in implementing a modern fullstack time tracking solution with Python and React.'
},
systemArchitecture: {
title: 'System Architecture',
frontend: {
title: 'Frontend (Next.js + TypeScript)',
description: 'The frontend architecture is based on Next.js with TypeScript and follows a component-based approach:',
code: {
types: `// types/TimeEntry.ts
interface TimeEntry {
id: number;
date: string;
checkIn: string;
checkOut: string | null;
userId: number;
status: 'complete' | 'incomplete';
}`,
component: `// components/TimeEntryForm.tsx
const TimeEntryForm: React.FC<TimeEntryFormProps> = ({ onSubmit }) => {
const [entry, setEntry] = useState<TimeEntry>({
date: new Date().toISOString().split('T')[0],
checkIn: new Date().toLocaleTimeString(),
checkOut: null,
status: 'incomplete'
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateTimeEntry(entry)) return;
try {
await onSubmit(entry);
} catch (error) {
console.error('Error submitting time entry:', error);
}
};
return (
<form onSubmit={handleSubmit} className='space-y-4'>
<DateInput
value={entry.date}
onChange={(date) => setEntry({ ...entry, date })}
/>
<TimeInput
value={entry.checkIn}
onChange={(time) => setEntry({ ...entry, checkIn: time })}
/>
{/* Additional form elements */}
</form>
);
};`
}
},
backend: {
title: 'Backend (Flask + MSSQL)',
description: 'The backend uses Flask for the API and MSSQL for data persistence:',
code: {
models: `# models/time_entry.py
class TimeEntry(db.Model):
__tablename__ = 'Stundenzettel'
id = db.Column('ID', db.Decimal, primary_key=True)
personal_id = db.Column('Personal_ID', db.Decimal)
datum = db.Column('Datum', db.Date)
kommen = db.Column('Kommen', db.Time)
gehen = db.Column('Gehen', db.Time)
@validates('gehen')
def validate_checkout(self, key, value):
if value and value < self.kommen:
raise ValueError('Checkout time cannot be before checkin time')
return value`,
routes: `# routes/time_entries.py
@app.route('/api/time-entries', methods=['POST'])
@jwt_required
def create_time_entry():
data = request.get_json()
user_id = get_jwt_identity()
try:
validate_time_entry_creation(data, user_id)
entry = TimeEntry(
personal_id=user_id,
datum=data['date'],
kommen=data['checkIn'],
gehen=data.get('checkOut')
)
db.session.add(entry)
db.session.commit()
return jsonify(entry.to_dict()), 201
except ValidationError as e:
return jsonify({'error': str(e)}), 400`
}
}
},
businessLogic: {
title: 'Business Logic Implementation',
validation: {
title: 'Time Entry Validation',
description: 'The validation logic ensures that all business rules are followed:',
code: `def validate_time_entry_creation(data: dict, user_id: int) -> None:
'''Validates a new time entry according to business rules.'''
# Check for existing incomplete entries
incomplete_entry = TimeEntry.query.filter_by(
personal_id=user_id,
gehen=None,
datum=data['date']
).first()
if incomplete_entry:
raise ValidationError('Cannot create new entry while incomplete entry exists')
# Check for time overlap with existing entries
overlapping_entry = TimeEntry.query.filter(
TimeEntry.personal_id == user_id,
TimeEntry.datum == data['date'],
TimeEntry.kommen <= data['checkIn'],
TimeEntry.gehen >= data['checkIn']
).first()
if overlapping_entry:
raise ValidationError('Time entry overlaps with existing entry')`
}
},
databaseDesign: {
title: 'Database Design',
description: 'The MSSQL database schema is designed for efficiency and integrity:',
code: `CREATE TABLE dbo.Stundenzettel (
ID decimal NOT NULL PRIMARY KEY,
Personal_ID decimal,
Datum date,
Kommen time,
Gehen time,
xStatus int,
xBenutzer nvarchar(15),
xDatum datetime,
xVersion timestamp
);
CREATE INDEX idx_personal_datum
ON dbo.Stundenzettel(Personal_ID, Datum);`
},
security: {
title: 'Security Implementation',
authentication: {
title: 'JWT Authentication',
code: `# auth/jwt_handler.py
from flask_jwt_extended import create_access_token
def authenticate_user(username: str, password: str) -> str:
user = Personal.query.filter_by(
Benutzername=username,
Passwort=password # In production, use proper password hashing
).first()
if not user:
raise AuthenticationError('Invalid credentials')
return create_access_token(identity=user.ID)`
}
},
bestPractices: {
title: 'Best Practices and Learnings',
points: [
{
title: 'Data Validation at Multiple Levels',
items: [
'Frontend validation for immediate feedback',
'Backend validation for business rules',
'Database constraints for data integrity'
]
},
{
title: 'Error Handling',
items: [
'Structured error messages',
'User-friendly error messages in frontend',
'Detailed logging in backend'
]
},
{
title: 'Performance Optimization',
items: [
'Indexing of critical database fields',
'Frontend caching of time entries',
'Lazy loading for historical data'
]
}
]
},
challenges: {
title: 'Challenges and Solutions',
timezones: {
title: '1. Timezone Handling',
description: 'A particular challenge was the correct handling of timezones:',
code: `// utils/dateTime.ts
export const formatTimeForDisplay = (time: string): string => {
return new Date(\`1970-01-01T\${time}\`).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
};
export const formatTimeForAPI = (time: string): string => {
return new Date(\`1970-01-01T\${time}\`).toISOString().split('T')[1];
};`
},
concurrency: {
title: '2. Concurrent Updates',
description: 'Handling concurrent updates required special attention:',
code: `from sqlalchemy import and_, or_
def update_time_entry(entry_id: int, data: dict) -> TimeEntry:
entry = TimeEntry.query.filter_by(id=entry_id).with_for_update().first()
if not entry:
raise NotFoundError('Time entry not found')
# Optimistic locking using version field
if entry.xVersion != data['version']:
raise ConcurrencyError('Entry was modified by another user')
entry.gehen = data.get('checkOut')
db.session.commit()
return entry`
},
offline: {
title: '3. Offline Capability',
description: 'For offline functionality, we implemented a service worker strategy:',
code: `// service-worker.ts
const CACHE_NAME = 'timetracking-v1';
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(response => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});`
}
},
conclusion: {
title: 'Conclusion',
description: 'Developing a time tracking solution requires careful planning and consideration of numerous business rules. By using modern technologies like Next.js, TypeScript, and Flask, we were able to implement a robust and user-friendly solution.',
keyPoints: [
'Strict typing with TypeScript',
'Comprehensive validation logic',
'Efficient database design',
'User-friendly error handling'
],
results: 'The solution has been successfully in use for several months and reliably processes hundreds of time entries daily.'
}
}
};
@@ -0,0 +1,204 @@
// src/i18n/locales/en/blog/posts/rfid-automation.ts
export const rfidAutomation = {
meta: {
title: 'RFID Label Automation: From Database to Printer',
date: '2024-02-09',
excerpt: 'A detailed technical analysis of implementing an automated RFID label printing system with Python',
category: 'System Integration',
coverImage: '/images/posts/rfid-automation/cover.jpg',
tags: ['RFID', 'Python', 'ZPL', 'Automation', 'Zebra Printer', 'EPC']
},
content: {
intro: {
title: 'RFID Label Automation: From Database to Printer',
description: 'In modern logistics and retail environments, efficient and error-free labeling of products with RFID tags is crucial. In this article, I share my experiences implementing an automated solution for generating and printing RFID labels with chip encoding.'
},
requirements: {
title: 'Project Background',
points: [
'Automatic generation of EPC codes (Electronic Product Code)',
'Creation of ZPL code for Zebra printers',
'Direct network communication with the printer',
'Integration with existing product data',
'Error handling and logging'
]
},
implementation: {
title: 'Technical Implementation',
epcGeneration: {
title: 'EPC Code Generation',
description: 'The EPC codes are generated according to the SGTIN standard (Serialized Global Trade Item Number):',
code: `def generate_encoded_epc(company_prefix, indicator, item_ref, serial):
sgtin = SGTIN(company_prefix, indicator, item_ref, serial)
return sgtin.encode()`
},
zplTemplates: {
title: 'ZPL Template Management',
description: 'ZPL templates were implemented for different label types:',
code: `def generate_zpl_code(artikelnr, description, ean, price, material, water_resistance, glass_type, encoded_epc):
formatted_price = f'{price:.2f}'
return f'''
CT~~CD,~CC^~CT~
^XA
~TA000
~JSN
^LT35
^MNW
^MTT
^PON
^PMN
^LH0,0
^JMA
^PR2,2
~SD23
^JUS
^LRN
^CI27
^PA0,1,1,0
^RS8,,,3
^XZ
^XA
^MMT
^PW413
^LL531
^LS-24
^FT150,57^A0N,33,33^FH\\^CI27^FD{artikelnr}^FS^CI27
^FT44,86^A0N,21,20^FH\\^CI27^FD{description}^FS^CI27
^FT130,141^A0N,50,51^FH\\^CI27^FD{formatted_price} €^FS^CI27
^FT167,175^A0N,21,20^FH\\^CI27^FD{material}^FS^CI27
^FT185,201^A0N,21,20^FH\\^CI27^FD{water_resistance}^FS^CI27
^FT155,227^A0N,21,20^FH\\^CI27^FD{glass_type}^FS^CI27
^BY3,2,113^FT73,420^BEN,,Y,N
^FH\\^FD{ean}^FS
^RFW,H,1,2,1^FD3000^FS
^RFW,H,2,12,1^FD{encoded_epc}^FS
^PQ1,0,1,Y
^XZ'''`
},
printerCommunication: {
title: 'Network Communication with the Printer',
description: 'Communication with the Zebra printer is done via TCP/IP:',
code: `def send_to_printer(data, printer_ip='192.168.68.50', printer_port=9100):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((printer_ip, printer_port))
sock.sendall(data.encode('utf-8'))
logger.info('Data successfully sent to printer')
except Exception as e:
logger.error(f'Error sending data to printer: {e}')`
},
serialNumberManagement: {
title: 'Data Processing and Serial Number Management',
description: 'Managing serial numbers is critical for the uniqueness of EPC codes:',
code: `def initialize_serial_number():
global current_serial_number
try:
serial_log_content = SERIAL_NUMBER_LOG_PATH.read_text().strip()
current_serial_number = int(serial_log_content)
except (FileNotFoundError, ValueError):
current_serial_number = START_SERIAL_NUMBER
def get_next_serial_number():
global current_serial_number
current_serial_number += 1
return current_serial_number
def finalize_serial_number():
SERIAL_NUMBER_LOG_PATH.write_text(str(current_serial_number))`
}
},
features: {
title: 'Implemented Features',
errorHandling: {
title: '1. Robust Error Handling',
points: [
'Input data validation',
'Network connection verification',
'Serial number persistence',
'Detailed logging of all operations'
]
},
configurability: {
title: '2. Configurability',
points: [
'Adjustable printer IP and port',
'Customizable ZPL templates',
'Flexible EPC code generation',
'Configurable serial number ranges'
]
},
scalability: {
title: '3. Scalability',
points: [
'Batch processing of product data',
'Parallel print jobs possible',
'Efficient resource management',
'Modular code for easy extensibility'
]
}
},
bestPractices: {
title: 'Best Practices',
dataValidation: {
title: '1. Data Validation',
points: [
'Strict typing for critical data fields',
'Verification of required fields',
'Format and plausibility checks'
]
},
faultTolerance: {
title: '2. Fault Tolerance',
points: [
'Automatic retry attempts',
'Graceful degradation',
'Detailed error logs'
]
},
maintainability: {
title: '3. Maintainability',
points: [
'Modular code structure',
'Comprehensive documentation',
'Clear separation of configuration and code'
]
}
},
lessonsLearned: {
title: 'Lessons Learned',
zplSpecifics: {
title: '1. ZPL Specifics',
points: [
'Detailed knowledge of ZPL specifications necessary',
'Careful validation of generated ZPL codes',
'Regular testing with different printer models'
]
},
rfidStandards: {
title: '2. RFID Standards',
points: [
'Adherence to EPC standards critical',
'Careful management of serial numbers',
'Validation of generated EPC codes'
]
},
networkCommunication: {
title: '3. Network Communication',
points: [
'Robust error handling for network issues',
'Timeouts and retry attempts',
'Buffering of print jobs'
]
}
},
conclusion: {
title: 'Conclusion',
description: 'The implemented solution enables efficient and reliable automation of the RFID labeling process. Through the combination of EPC code generation, ZPL template management, and direct printer communication, a robust system has been created that has proven itself in practice.',
keyPoints: [
'Thorough planning of system architecture',
'Comprehensive error handling',
'Careful documentation',
'Regular testing and validation'
],
results: 'The solution has been running stably in production for several months and processes hundreds of labels daily.'
}
}
};
+99
View File
@@ -0,0 +1,99 @@
export const common = {
// SEO & Meta
siteTitle: 'Damjan Savić - Digital Solutions & JTL Integration',
siteDescription: 'Developer of custom solutions for process automation',
// Navigation
nav: {
home: 'Home',
about: 'About',
services: 'Services',
portfolio: 'Portfolio',
blog: 'Blog',
contact: 'Contact'
},
// Common Actions
actions: {
readMore: 'Read More',
viewMore: 'View More',
viewAll: 'View All',
back: 'Back',
close: 'Close',
submit: 'Submit',
loading: 'Loading...'
},
// UI Components
ui: {
published: 'Published on',
lastUpdated: 'Last updated on',
category: 'Category',
categories: 'Categories',
tag: 'Tag',
tags: 'Tags',
author: 'Author',
minuteRead: 'minute read',
tableOfContents: 'Table of Contents'
},
// Error Messages
errors: {
generic: 'An error has occurred',
notFound: 'Page not found',
loading: 'Error loading',
return: 'Return to homepage'
},
// Loading States
loading: {
content: 'Loading content',
page: 'Loading page',
processing: 'Processing'
},
// Dates
dates: {
months: {
long: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
],
short: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
},
weekdays: {
long: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday'
],
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
}
},
// Social Media
social: {
followMe: 'Follow me on',
shareOn: 'Share on',
linkedin: 'LinkedIn',
github: 'GitHub',
email: 'Email'
},
// Contact Form
contact: {
title: 'Contact',
name: 'Name',
email: 'Email',
message: 'Message',
submit: 'Send Message',
success: 'Your message has been sent successfully',
error: 'There was an error sending your message'
},
// Accessibility
a11y: {
skipToContent: 'Skip to main content',
menuOpen: 'Open menu',
menuClose: 'Close menu',
darkMode: 'Toggle dark mode',
carousel: {
next: 'Next image',
previous: 'Previous image'
}
}
} as const;
export type CommonTranslations = typeof common;
+9
View File
@@ -0,0 +1,9 @@
const cookiebanner = {
cookies: {
message: "This website uses cookies to enhance your browsing experience. The data will not be shared with third parties.",
accept: "Accept Cookies",
decline: "Decline"
}
};
export default cookiebanner;
+40
View File
@@ -0,0 +1,40 @@
// src/i18n/locales/en/footer.ts
const footer = {
sections: {
contact: {
title: "Contact",
email: "info@damjan-savic.com",
location: "Cologne, Germany",
aria: {
emailLink: "Send email",
locationText: "Location"
}
},
navigation: {
title: "Navigation",
links: {
portfolio: "Portfolio",
blog: "Blog",
about: "About",
contact: "Contact"
}
},
social: {
title: "Social Media",
aria: {
linkedin: "Visit my LinkedIn profile",
github: "Visit my GitHub profile"
}
}
},
legal: {
copyright: "All rights reserved",
links: {
privacy: "Privacy Policy",
terms: "Legal Notice"
}
}
} as const;
export default footer;
export type FooterTranslations = typeof footer;
+109
View File
@@ -0,0 +1,109 @@
// src/i18n/locales/en/index.ts
// Blog post translations
import { erpIntegrationBreuninger } from './blog/posts/erp-integration-breuninger';
import { fullstackDevelopmentTimetracking } from './blog/posts/fullstack-development-timetracking';
import { rfidAutomation as blogRfidAutomation } from './blog/posts/rfid-automation';
import { blog } from './blog';
// Page translations
import { about } from './pages/about/index';
import { contact } from './pages/contact/index';
import { dashboard } from './pages/dashboard';
import { home } from './pages/home/index';
import { login } from './pages/login';
import { notfound } from './pages/notfound';
import { portfolio as portfolioPage } from './pages/portfolio';
import { privacy } from './pages/privacy';
import { terms } from './pages/terms';
// Portfolio project translations
import { aiDataReader } from './portfolio/projects/ai-data-reader';
import { portfolio } from './portfolio';
// Common translations and configurations
import { common } from './common';
import { meta } from './meta';
import { navigation } from './navigation';
// Footer translations
import footer from './footer';
// Merging all translation objects
const translations = {
// Pages
pages: {
about,
contact,
dashboard,
home,
login,
notfound,
portfolio: portfolioPage,
privacy,
terms,
},
// Blog
blog: {
...blog,
posts: {
erpIntegrationBreuninger,
fullstackDevelopmentTimetracking,
rfidAutomation: blogRfidAutomation,
},
},
// Portfolio
portfolio: {
...portfolio,
projects: {
aiDataReader,
},
},
// Footer
footer,
// Common configurations
common: {
...common,
cookies: {
message: "This website uses cookies to enhance your browsing experience and provide personalized content. The collected data will not be shared with third parties.",
accept: "Accept Cookies",
decline: "Decline"
}
},
meta,
navigation,
} as const;
// Default export: complete translations object
export default translations;
// Type definition for translations
export type Translations = typeof translations;
// Re-exports for direct access to individual modules
export {
// Pages
about,
contact,
dashboard,
home,
login,
notfound,
portfolioPage as portfolioPageTranslations,
privacy,
terms,
// Blog
blog,
erpIntegrationBreuninger,
fullstackDevelopmentTimetracking,
blogRfidAutomation as blogRfidAutomationPost,
// Portfolio
portfolio,
aiDataReader,
// Footer
footer,
// Common
common,
meta,
navigation,
};
+93
View File
@@ -0,0 +1,93 @@
export const meta = {
site: {
name: 'Damjan Savić',
title: 'Damjan Savić - Digital Solutions & JTL Integration',
subtitle: 'Developer of custom solutions for process automation',
description: 'With a background in software development and expertise in digital transformation, I help companies optimize and automate their business processes.',
keywords: [
'JTL Integration',
'E-Commerce Solutions',
'Process Automation',
'Digital Consulting',
'ERP Systems',
'Python Development',
'System Integration',
'Digital Marketing'
].join(', ')
},
company: {
name: 'Damjan Savić',
shortName: 'Damjan Savić',
description: 'Your software development partner for fast, reliable, and cost-efficient solutions.',
address: 'Rotdornallee',
city: 'Köln',
postalCode: '50999',
country: 'Germany',
phone: '+49 175 695 0979',
email: 'info@damjan-savic.com',
website: 'www.damjan-savic.com'
},
author: {
name: 'Damjan Savić',
role: 'Consultant Digital Solutions',
company: 'Ritter Digital GmbH',
location: 'Oberhausen, Germany',
email: 'info@damjan-savic.com',
website: 'www.damjan-savic.com',
languages: [
{ code: 'en', level: 'C2', name: 'English' },
{ code: 'sr', level: 'C2', name: 'Serbian' },
{ code: 'de', level: 'C2', name: 'German' },
{ code: 'fr', level: 'B2', name: 'French' },
{ code: 'es', level: 'B2', name: 'Spanish' },
{ code: 'ru', level: 'A1', name: 'Russian' }
]
},
social: {
linkedin: 'https://www.linkedin.com/in/damjansavic/',
github: 'https://github.com/damjansavic',
email: 'info@damjan-savic.com'
},
expertise: {
areas: [
'JTL Integration & Consulting',
'Process Automation',
'E-Commerce Development',
'System Integration',
'ERP Systems',
'Digital Transformation'
],
technologies: [
'Python',
'React/Next.js',
'MariaDB',
'Docker',
'JTL-WaWi',
'Shopify',
'Shopware'
]
},
seo: {
defaultLanguage: 'en',
alternateLanguages: ['de', 'sr'],
robots: 'index, follow',
googleSiteVerification: '', // If needed
og: {
type: 'website',
locale: 'en_US',
siteName: 'Damjan Savić - Digital Solutions',
images: {
url: '/images/og-image.jpg',
width: 1200,
height: 630,
alt: 'Damjan Savić - Digital Solutions & JTL Integration'
}
},
twitter: {
card: 'summary_large_image',
site: '@damjansavic',
creator: '@damjansavic'
}
}
} as const;
export type MetaConfig = typeof meta;
+79
View File
@@ -0,0 +1,79 @@
// C:\Development\Damjan Savic\Portfolio\src\i18n\locales\en\navigation.ts
export const navigation = {
// Main navigation
main: {
home: 'Home',
about: 'About',
portfolio: 'Portfolio',
blog: 'Blog',
contact: 'Contact'
},
// Dashboard and Admin
admin: {
login: 'Login',
dashboard: 'Dashboard',
logout: 'Logout'
},
// Footer Navigation
footer: {
legal: {
privacy: 'Privacy Policy',
terms: 'Terms & Conditions',
imprint: 'Imprint'
},
sections: {
portfolio: 'Portfolio',
blog: 'Blog',
contact: 'Contact'
}
},
// Page sections
sections: {
experience: 'Experience',
skills: 'Skills',
projects: 'Projects',
about: 'About'
},
// Breadcrumbs and back navigation
navigation: {
backTo: {
home: 'Back to Home',
portfolio: 'Back to Portfolio',
blog: 'Back to Blog'
},
breadcrumbs: {
home: 'Home',
portfolio: 'Portfolio',
blog: 'Blog',
about: 'About',
contact: 'Contact'
}
},
// Mobile Navigation
mobile: {
menu: 'Menu',
close: 'Close',
open: 'Open Menu'
},
// URLs (for dynamic route generation)
urls: {
home: '/',
about: '/about',
portfolio: '/portfolio',
blog: '/blog',
contact: '/contact',
login: '/login',
dashboard: '/dashboard',
privacy: '/privacy',
terms: '/terms'
},
// Meta information for navigation
meta: {
main: 'Main Navigation',
footer: 'Footer Navigation',
social: 'Social Media Navigation',
legal: 'Legal Navigation'
}
} as const;
export type NavigationConfig = typeof navigation;
+33
View File
@@ -0,0 +1,33 @@
export const hero = {
// Navigation/Header
aboutMe: 'ABOUT ME',
// Main Section
title: 'Digital Solutions Expert',
description: 'With a background in software development and expertise in digital transformation, I help companies optimize and automate their processes through innovative solutions.',
// Expertise Areas
expertise: {
processAutomation: {
title: 'Process Automation',
description: 'Development of efficient automated workflows'
},
systemIntegration: {
title: 'System Integration',
description: 'Connection and optimization of business systems'
},
customDevelopment: {
title: 'Custom Development',
description: 'Creation of tailored software solutions'
}
},
// Languages
languages: {
english: 'English',
serbian: 'Serbian',
german: 'German',
french: 'French',
spanish: 'Spanish',
russian: 'Russian'
}
} as const;
// Type for type safety
export type HeroTranslations = typeof hero;
+24
View File
@@ -0,0 +1,24 @@
import { hero } from './hero';
import { journey } from './journey';
import { schema } from './schema';
import { skillbar } from './skillbar';
import { skillgroup } from './skillgroup';
import { skills } from './skills';
import { workflow } from './workflow';
export const about = {
hero,
journey,
schema,
skillbar,
skillgroup,
skills,
workflow
} as const;
// Typen exportieren
export type { HeroTranslations } from './hero';
export type { JourneyTranslations } from './journey';
export type { SchemaTranslations } from './schema';
export type { SkillBarTranslations } from './skillbar';
export type { SkillGroupTranslations } from './skillgroup';
export type { SkillsTranslations } from './skills';
export type { WorkflowTranslations } from './workflow';
@@ -0,0 +1,95 @@
export const journey = {
// Header
title: 'Professional Journey',
subtitle: 'From digital media to enterprise solutions',
// Education Section
education: {
title: 'Education',
degrees: {
masters: {
degree: 'M.A. Software Development',
university: 'Middlesex University, London',
period: '2020 - 2022'
},
bachelors: {
degree: 'B.A. Cross-Media Production',
university: 'Middlesex University, London',
period: '2018 - 2020'
}
}
},
// Languages Section
languages: {
title: 'Languages',
items: {
english: {
name: 'English',
level: 'C2'
},
serbian: {
name: 'Serbian',
level: 'C2'
},
german: {
name: 'German',
level: 'C2'
},
french: {
name: 'French',
level: 'B2'
},
spanish: {
name: 'Spanish',
level: 'B2'
},
russian: {
name: 'Russian',
level: 'A1'
}
}
},
// Work History
work: {
positions: [
{
period: '08/2023 - Present',
company: 'Ritter Digital GmbH',
title: 'Digital Solutions Consultant',
location: 'Oberhausen',
highlights: [
'ERP consulting and implementation (JTL WaWi)',
'Process automation & full-stack development',
'RFID technology & automation',
'Lead generation & management',
'AI-supported content management'
]
},
{
period: '02/2023 - 08/2023',
company: 'Joyce & Girls Co AG',
title: 'ERP Integration Specialist',
location: 'Köln',
highlights: [
'Complex system integration (Apparel Magic, TradeByte)',
'Server administration and automation',
'Database and API development',
'Digital marketing and campaign analysis'
]
},
{
period: '08/2022 - 01/2023',
company: 'Magnarius',
title: 'RPA Developer & E-Commerce Manager',
location: 'Bergisch Gladbach',
highlights: [
'Shopware 6 development and JTL integration',
'Google Ads automation',
'CMS management and optimization',
'SEO strategy implementation'
]
}
]
}
} as const;
// Type for type safety
export type JourneyTranslations = typeof journey;
+31
View File
@@ -0,0 +1,31 @@
export const schema = {
page: {
name: 'About Damjan Savić',
description: 'Learn more about Damjan Savić, a certified JTL expert with extensive experience in e-commerce and warehouse management solutions.'
},
person: {
jobTitle: 'JTL Integration Expert',
description: 'With extensive experience in integrating and optimizing e-commerce systems, I help companies with the digital transformation of their business processes.',
languages: {
english: 'English',
serbian: 'Serbian',
german: 'German',
french: 'French',
spanish: 'Spanish',
russian: 'Russian'
},
skills: {
python: 'Python',
googleAds: 'Google Ads',
metaAds: 'Meta Ads',
shopware: 'Shopware',
shopify: 'Shopify',
wooCommerce: 'WooCommerce',
jtlWawi: 'JTL WAWI',
serverAdmin: 'Server Administration',
airflow: 'AirFlow'
}
}
} as const;
// Type for type safety
export type SchemaTranslations = typeof schema;
@@ -0,0 +1,13 @@
export const skillbar = {
// Accessibility labels
ariaLabel: {
skillLevel: 'Skill Level',
progressBar: 'Progress Bar'
},
// Screen reader text
screenReader: {
progress: '{{level}} percent progress for {{skill}}'
}
} as const;
// Type for type safety
export type SkillBarTranslations = typeof skillbar;
@@ -0,0 +1,63 @@
export const skillgroup = {
// Categories
categories: {
development: 'Development',
ecommerce: 'E-Commerce',
marketing: 'Marketing',
automation: 'Automation',
languages: 'Programming Languages',
databases: 'Databases'
},
// Skills by category
skills: {
development: {
frontend: 'Frontend Development',
backend: 'Backend Development',
fullstack: 'Full-Stack Development',
api: 'API Development',
testing: 'Software Testing'
},
ecommerce: {
jtl: 'JTL WaWi',
shopware: 'Shopware',
shopify: 'Shopify',
woocommerce: 'WooCommerce',
magento: 'Magento'
},
marketing: {
seo: 'Search Engine Optimization',
sea: 'Search Engine Advertising',
analytics: 'Web Analytics',
googleAds: 'Google Ads',
metaAds: 'Meta Ads'
},
automation: {
rpa: 'Robotic Process Automation',
workflow: 'Workflow Automation',
cicd: 'CI/CD',
testing: 'Test Automation',
deployment: 'Deployment Automation'
},
languages: {
python: 'Python',
javascript: 'JavaScript',
typescript: 'TypeScript',
php: 'PHP',
sql: 'SQL'
},
databases: {
mysql: 'MySQL',
postgresql: 'PostgreSQL',
mongodb: 'MongoDB',
redis: 'Redis',
elasticsearch: 'Elasticsearch'
}
},
// Accessibility
ariaLabels: {
skillGroup: 'Skill Group',
skillList: 'List of skills in this category'
}
} as const;
// Type for type safety
export type SkillGroupTranslations = typeof skillgroup;
+46
View File
@@ -0,0 +1,46 @@
// en/pages/about/skills.ts
export const skills = {
title: 'Skills',
skillGroups: [
{
category: 'Development',
items: [
{ name: 'Python', level: 90 },
{ name: 'Google Ads', level: 85 },
{ name: 'Meta Ads', level: 85 }
]
},
{
category: 'E-Commerce',
items: [
{ name: 'Shopware', level: 85 },
{ name: 'Shopify', level: 85 },
{ name: 'WooCommerce', level: 85 }
]
},
{
category: 'System Administration',
items: [
{ name: 'JTL WAWI', level: 85 },
{ name: 'Server', level: 75 },
{ name: 'AirFlow', level: 60 }
]
}
],
ui: {
percentage: 'percent',
levelLabel: 'Level',
skillLevel: '{{level}}% proficiency in {{skill}}'
},
aria: {
skillGroup: 'Skill group for {{category}}',
skillBar: 'Progress bar for {{skill}}',
skillIcon: 'Icon for {{category}}',
selectSkill: 'Select skill'
},
screenReader: {
skillProgress: '{{skill}} with a level of {{level}} percent'
}
} as const;
export type SkillsTranslations = typeof skills;
@@ -0,0 +1,44 @@
export const workflow = {
// Page title
title: 'MY WORKFLOW',
description: 'Systematic approach to software development through defined steps',
// Workflow steps
steps: [
{
number: '01',
title: 'REQUIREMENTS ANALYSIS'
},
{
number: '02',
title: 'ASSESSMENT OF DEADLINES AND CONSTRAINTS'
},
{
number: '03',
title: 'DEVELOPMENT ENVIRONMENT SETUP'
},
{
number: '04',
title: 'PROGRAMMING'
},
{
number: '05',
title: 'TESTING THE RESULTS'
},
{
number: '06',
title: 'CODE SUPPORT AND SCALING'
}
],
// Aria Labels for accessibility
aria: {
workflowSection: 'Workflow Process',
stepNumber: 'Step {{number}}',
stepDescription: 'Workflow step: {{title}}'
},
// Screen Reader texts
screenReader: {
stepProgress: 'Step {{number}} of {{total}}: {{title}}'
}
} as const;
// Type for type safety
export type WorkflowTranslations = typeof workflow;
@@ -0,0 +1,41 @@
// src/i18n/locales/en/pages/contact/contactform.ts
export const contactform = {
// Form Header
title: 'Contact Me',
description: 'Have questions or want to collaborate? Send me a message.',
// Form Fields
name: {
label: 'Name',
placeholder: 'Your full name'
},
email: {
label: 'Email',
placeholder: 'your.email@example.com'
},
message: {
label: 'Message',
placeholder: 'Your message to me...'
},
// Button
submit: 'Send Message',
// Status Messages
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. Please try again later.',
// Error Messages
errors: {
nameRequired: 'Please enter your name',
emailRequired: 'Please enter your email address',
emailInvalid: 'Please enter a valid email address',
messageRequired: 'Please enter a message'
},
// Aria Labels
aria: {
form: 'Contact form',
submitting: 'Sending form',
successAlert: 'Success message',
errorAlert: 'Error message'
}
} as const;
// Type for type safety
export type ContactFormTranslations = typeof contactform;
@@ -0,0 +1,27 @@
// src/i18n/locales/en/pages/contact/contactinfo.ts
export const contactinfo = {
// Header
title: 'Contact Information',
description: 'You can reach me through the following channels',
// Contact Method Labels
addressLabel: 'Office Location',
phoneLabel: 'Phone',
emailLabel: 'Email',
// Contact Information
address: 'Rotdornallee, Köln',
phone: '+49 175 695 0979',
email: 'info@damjan-savic.com',
// Additional Information
availabilityNote: 'Business hours: Monday to Friday, 9:00 AM - 5:00 PM',
// Aria Labels
aria: {
contactCard: 'Contact information card',
addressLink: 'Open location in Google Maps',
phoneLink: 'Call',
emailLink: 'Send email',
externalLink: 'Opens in new tab'
}
} as const;
// Type for type safety
export type ContactInfoTranslations = typeof contactinfo;
@@ -0,0 +1,71 @@
// src/i18n/locales/en/pages/contact/index.ts
export const contact = {
breadcrumb: "CONTACT",
hero: {
title: "Let's Work Together",
subtitle: "Have a project in mind? Let's discuss how we can help you achieve your goals."
},
contactInfo: {
title: "Contact Information",
description: "You can reach me through the following channels",
address: {
label: "Office Location",
value: "Rotdornallee, Köln"
},
phone: {
label: "Phone",
value: "+49 175 695 0979"
},
email: {
label: "Email",
value: "info@damjan-savic.com"
},
availabilityNote: "Business hours: Monday to Friday, 9:00 AM - 5:00 PM",
aria: {
contactCard: "Contact information card",
addressLink: "Open location in Google Maps",
phoneLink: "Call",
emailLink: "Send email",
externalLink: "Opens in new tab"
}
},
contactForm: {
title: "Contact Me",
description: "Have questions or want to collaborate? Send me a message.",
name: {
label: "Name",
placeholder: "Your full name"
},
email: {
label: "Email",
placeholder: "your.email@example.com"
},
message: {
label: "Message",
placeholder: "Your message to me..."
},
submit: "Send 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. Please try again later.",
errors: {
nameRequired: "Please enter your name",
emailRequired: "Please enter your email address",
emailInvalid: "Please enter a valid email address",
messageRequired: "Please enter a message"
},
aria: {
form: "Contact form",
submitting: "Sending form",
successAlert: "Success message",
errorAlert: "Error message"
}
}
} as const;
export type ContactTranslations = typeof contact;
export default {
pages: {
contact: contact
}
};
+76
View File
@@ -0,0 +1,76 @@
// src/i18n/locales/en/pages/dashboard.ts
export const dashboard = {
// SEO
seo: {
title: 'Analytics Dashboard',
description: 'Website Analysis Dashboard'
},
// Main headings
header: {
title: 'Analytics Dashboard',
activeUsers: 'active users'
},
// Metric cards
metrics: {
pageViews: {
title: 'Page Views',
perMinute: 'per minute'
},
uniqueVisitors: {
title: 'Unique Visitors',
currentlyActive: 'currently active'
},
conversions: {
title: 'Conversions',
rate: 'conversion rate'
},
timeOnSite: {
title: 'Avg. Time on Site',
bounceRate: 'bounce rate'
}
},
// Top pages section
topPages: {
title: 'Top Pages',
columns: {
path: 'Path',
views: 'Views',
change: 'Change'
}
},
// Event tracking section
events: {
title: 'Event Tracking',
types: {
contactForm: 'Contact Form Submission',
portfolioView: 'Portfolio View',
cvDownload: 'CV Download',
blogRead: 'Blog Read'
}
},
// Status messages
status: {
loading: 'Loading Dashboard...',
error: 'Error loading data',
notAuthenticated: 'Please sign in'
},
// Miscellaneous
misc: {
viewMore: 'View More',
refresh: 'Refresh',
period: {
today: 'Today',
yesterday: 'Yesterday',
last7Days: 'Last 7 Days',
last30Days: 'Last 30 Days',
thisMonth: 'This Month',
lastMonth: 'Last Month'
}
},
// Trends and changes
trends: {
increase: 'Increase',
decrease: 'Decrease',
noChange: 'No Change'
}
};
+9
View File
@@ -0,0 +1,9 @@
// src/i18n/locales/en/pages/home/about.ts
export const about = {
title: 'About Me',
content: 'As a digital transformation consultant, I help companies improve their processes through modern software. I develop solutions for connecting inventory management systems, programs for workflow automation, and RFID applications for better product tracking. At Ritter Digital GmbH, I work with JTL software and develop custom program solutions. Another project involved connecting Breuninger\'s systems with Apparel Magic via TB.One. My goal is to make companies more efficient through software.',
image: {
alt: 'Portrait of Damjan Savić'
}
} as const;
export type AboutTranslations = typeof about;
@@ -0,0 +1,77 @@
// src/i18n/locales/en/pages/home/experience.ts
export const experience = {
title: 'Professional Experience',
navigation: {
prev: '←',
next: '→'
},
positions: [
{
role: 'Consultant Digital Solutions & Developer',
company: 'RITTER Gesellschaft für digitale Geschäftsprozesse mbH',
period: '08/2023 - PRESENT',
highlights: [
'JTL WaWi, WMS and POS consulting & implementation',
'Python-based automation solutions',
'RFID systems and GUI development',
'AI & Business Intelligence'
]
},
{
role: 'ERP Integration Specialist & E-Commerce Developer',
company: 'Joyce & Girls',
period: '01/2023 - 08/2023',
highlights: [
'ApparelMagic & TradeByte integration',
'MariaDB middleware development',
'Windows Server administration',
'Shopify optimization'
]
},
{
role: 'RPA Developer & E-Commerce Manager',
company: 'C&S Marketing',
period: '08/2022 - 01/2023',
highlights: [
'Shopware 6 & JTL integration',
'Python automation',
'Google Ads optimization',
'CMS management'
]
},
{
role: 'E-Commerce Developer',
company: 'Brands Club GmbH',
period: '08/2021 - 07/2022',
highlights: [
'Shopify & JTL integration',
'Server administration',
'Content production',
'Marketing automation'
]
},
{
role: 'E-Commerce Developer',
company: 'Feine Uhren Eupen',
period: '01/2021 - 08/2021',
highlights: [
'Shopware 5 development',
'Product photography',
'Luxury watch segment sales',
'E-Commerce management'
]
},
{
role: 'Online Marketing Manager',
company: 'Ufer8, Ambis, Teatro & Die Halle Tor 2',
period: '01/2018 - 12/2020',
highlights: [
'Social media content',
'Event marketing',
'Event organization',
'Marketing automation'
]
}
]
} as const;
export type ExperienceTranslations = typeof experience;

Some files were not shown because too many files have changed in this diff Show More