diff --git a/public/images/portfolio-placeholder.svg b/public/images/portfolio-placeholder.svg
new file mode 100644
index 0000000..d9e172f
--- /dev/null
+++ b/public/images/portfolio-placeholder.svg
@@ -0,0 +1 @@
+
diff --git a/src/content/contracts.ts b/src/content/contracts.ts
new file mode 100644
index 0000000..c68b754
--- /dev/null
+++ b/src/content/contracts.ts
@@ -0,0 +1,80 @@
+import { z } from 'zod';
+
+export const sectionKeys = ['hero', 'about', 'projects', 'experience', 'skills', 'education', 'milestones', 'footer'] as const;
+export const entryKinds = ['projects', 'experience', 'skills', 'education', 'milestones'] as const;
+const text = z.string().max(20000);
+const title = z.string().min(1).max(300);
+export const slugSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100);
+export const linkSchema = z.string().max(2048).refine(value => {
+ if (!value) return true;
+ if (/^\/media\/[a-zA-Z0-9.-]+$/.test(value)) return true;
+ try { return ['https:', 'http:', 'mailto:', 'tel:'].includes(new URL(value).protocol); } catch { return false; }
+}, 'Use an https, http, mailto, tel, or uploaded-media URL');
+export const imageSchema = z.object({
+ src: z.string().regex(/^\/(?:images|media)\/[a-zA-Z0-9/_().-]+$/).refine(value => !value.includes('..'), 'Invalid image path'),
+ width: z.number().int().min(1).max(20000), height: z.number().int().min(1).max(20000),
+}).strict();
+const strings = z.array(text).max(200);
+const gallery = z.array(z.object({ image: imageSchema, alt: text, caption: text }).strict()).max(100);
+export const entrySchemas = {
+ projects: z.object({
+ title, liveUrl: linkSchema, date: text, description: text, tags: strings, image: imageSchema,
+ caseStudy: z.object({
+ introduction: text, role: text, targetUsers: text, status: text, challenge: text,
+ ownership: strings, decisions: z.array(z.object({ discipline: z.enum(['product', 'engineering', 'design']), title, description: text }).strict()).max(100),
+ outcomes: strings, skills: strings, tools: strings, team: text, constraints: strings,
+ metrics: z.array(z.object({ value: text, label: text }).strict()).max(100), lessons: strings, gallery,
+ }).strict(),
+ }).strict(),
+ experience: z.object({ role: title, company: title, date: text, location: text, logo: imageSchema, isCurrent: z.boolean(), summary: text, bullets: strings }).strict(),
+ education: z.object({ degree: title, institution: title, date: text, description: text, logo: imageSchema }).strict(),
+ milestones: z.object({ title, issuer: text, date: text, description: text, image: imageSchema, link: linkSchema }).strict(),
+ skills: z.object({ label: title, skills: z.array(z.object({ name: title, level: z.number().int().min(0).max(5) }).strict()).max(100), keywords: strings, tools: z.array(z.object({ id: slugSchema, name: title, icon: imageSchema }).strict()).max(100) }).strict(),
+};
+export const sectionSchemas = {
+ hero: z.object({ sectionLabel: text, titleLabel: text, greeting: text, name: title, subtitle: text, avatarAlt: text }).strict(),
+ about: z.object({ eyebrow: text, title, description: text, steps: z.array(z.object({ title, description: text }).strict()).max(20) }).strict(),
+ projects: z.object({ eyebrow: text, title }).strict(),
+ experience: z.object({ title }).strict(),
+ skills: z.object({ eyebrow: text, title }).strict(),
+ education: z.object({ title }).strict(),
+ milestones: z.object({ eyebrow: text, title }).strict(),
+ footer: z.object({ title, subtitle: text }).strict(),
+};
+export const settingsSchema = z.object({
+ defaultLanguage: z.string().min(2).max(35), avatar: imageSchema,
+ contactUrl: linkSchema, blogUrl: linkSchema, coursesUrl: linkSchema, email: z.union([z.email(), z.literal('')]),
+ contacts: z.array(z.object({ id: z.enum(['phone', 'linkedin', 'github', 'telegram']), label: text, href: linkSchema }).strict()).max(4)
+ .refine(items => new Set(items.map(item => item.id)).size === items.length, 'Contact types must be unique'),
+ metadata: z.record(z.string(), z.object({ title, description: text, keywords: strings }).strict()),
+}).strict();
+export const languageSchema = z.object({
+ code: z.string().min(2).max(35).refine(value => {
+ try { return Intl.getCanonicalLocales(value)[0].toLowerCase() === value && !['admin','api','media','projects'].includes(value); } catch { return false; }
+ }, 'Use a lowercase language tag, such as en, fa, or pt-br'),
+ name: title, direction: z.enum(['ltr', 'rtl']), enabled: z.boolean(),
+ messages: z.record(z.string().max(150), z.string().max(2000)),
+}).strict();
+export const entrySchema = z.object({
+ id: slugSchema, kind: z.enum(entryKinds), position: z.number().int().min(0).max(100000), visible: z.boolean(),
+ shared: z.record(z.string(), z.unknown()), translations: z.record(z.string(), z.record(z.string(), z.unknown())),
+}).strict();
+export const sectionSchema = z.object({
+ key: z.enum(sectionKeys), visible: z.boolean(), translations: z.record(z.string(), z.record(z.string(), z.unknown())),
+}).strict();
+export type Settings = z.infer;
+export type Language = z.infer;
+export type Entry = z.infer;
+export type Section = z.infer;
+export type EntryKind = typeof entryKinds[number];
+export type SectionKey = typeof sectionKeys[number];
+export type EntryData = z.infer<(typeof entrySchemas)[K]> & { id: string };
+
+export function formatMessage(template: string, values: Record, locale = 'en') {
+ return template.replace(/\{(\w+)\}/g, (match, key: string) => {
+ const value = values[key];
+ return value === undefined ? match : typeof value === 'number' ? new Intl.NumberFormat(locale).format(value) : value;
+ });
+}
+export function portfolioPath(locale: string, defaultLanguage: string) { return locale === defaultLanguage ? '/' : `/${locale}`; }
+export function projectPath(locale: string, id: string, defaultLanguage: string) { return `${locale === defaultLanguage ? '' : `/${locale}`}/projects/${id}`; }
diff --git a/src/content/messages.en.json b/src/content/messages.en.json
new file mode 100644
index 0000000..16d9e45
--- /dev/null
+++ b/src/content/messages.en.json
@@ -0,0 +1,99 @@
+{
+ "notFound": {
+ "code": "404",
+ "title": "This page isn’t here",
+ "description": "The address may have changed, or the page may no longer exist.",
+ "backHome": "Back to portfolio"
+ },
+ "hero": {
+ "themeLightLabel": "Switch to light mode",
+ "themeDarkLabel": "Switch to dark mode",
+ "languageButtonLabel": "Choose language",
+ "languageMenuLabel": "Languages",
+ "englishLabel": "English",
+ "persianLabel": "فارسی",
+ "blog": "Blog",
+ "blogLabel": "Blog, coming soon",
+ "courses": "Courses",
+ "coursesLabel": "Courses, coming soon",
+ "comingSoon": "Coming soon",
+ "contact": "Get in touch",
+ "contactLabel": "Get in touch"
+ },
+ "about": {
+ "journeyLabel": "Career journey"
+ },
+ "projects": {
+ "previousLabel": "Previous projects",
+ "nextLabel": "Next projects",
+ "carouselLabel": "Scrollable project cards",
+ "carouselRoleDescription": "carousel",
+ "scrollbarLabel": "Project carousel position",
+ "viewCaseStudy": "View case study",
+ "liveProjectLabel": "Open the live {title} project in a new tab",
+ "drawerLabel": "Project case study",
+ "closeCaseStudy": "Close case study",
+ "shareLabel": "Share",
+ "shareCaseStudy": "Share this case study",
+ "shareDialogTitle": "Share case study",
+ "shareDialogDescription": "Send this project to someone or copy its direct link.",
+ "closeShareDialog": "Close sharing options",
+ "shareTelegram": "Telegram",
+ "shareWhatsApp": "WhatsApp",
+ "shareLinkedIn": "LinkedIn",
+ "shareEmail": "Email",
+ "copyLink": "Copy link",
+ "linkCopied": "Link copied",
+ "copyFailed": "Could not copy the link",
+ "roleLabel": "Role",
+ "usersLabel": "Users",
+ "statusLabel": "Status",
+ "teamLabel": "Team",
+ "galleryLabel": "Product gallery",
+ "openGalleryImage": "Open full-screen image: {caption}",
+ "selectGalleryImage": "Show image {index}: {caption}",
+ "imageViewerLabel": "Full-screen project gallery",
+ "closeImageViewer": "Close full-screen image",
+ "previousImage": "Previous image",
+ "nextImage": "Next image",
+ "imageCounter": "{current} of {total}",
+ "challengeLabel": "The challenge",
+ "ownershipLabel": "My ownership",
+ "decisionsLabel": "Key decisions",
+ "outcomesLabel": "Outcomes",
+ "skillsLabel": "Skills",
+ "toolsLabel": "Tools",
+ "constraintsLabel": "Constraints",
+ "metricsLabel": "Results",
+ "lessonsLabel": "Lessons",
+ "previousCaseStudy": "Previous project",
+ "nextCaseStudy": "Next project",
+ "decisionLabels": {
+ "product": "Product",
+ "engineering": "Engineering",
+ "design": "Design"
+ }
+ },
+ "experience": {
+ "logoAlt": "{company} logo"
+ },
+ "skills": {
+ "skillsLabel": "SKILLS",
+ "toolsLabel": "TOOLS",
+ "filterLabel": "Choose skill discipline"
+ },
+ "education": {},
+ "milestones": {
+ "viewCertificate": "View certificate",
+ "viewCertificateLabel": "View {title} certificate"
+ },
+ "footer": {
+ "emailLabel": "Send email",
+ "contactsLabel": "Social and contact links",
+ "sceneLabel": "Interactive 3D isometric footer scene",
+ "phone": "Phone",
+ "linkedin": "LinkedIn",
+ "github": "GitHub",
+ "telegram": "Telegram"
+ }
+}
diff --git a/src/content/messages.fa.json b/src/content/messages.fa.json
new file mode 100644
index 0000000..5931e4f
--- /dev/null
+++ b/src/content/messages.fa.json
@@ -0,0 +1,99 @@
+{
+ "notFound": {
+ "code": "۴۰۴",
+ "title": "این صفحه پیدا نشد",
+ "description": "ممکن است نشانی صفحه تغییر کرده باشد یا دیگر در دسترس نباشد.",
+ "backHome": "بازگشت به نمونهکارها"
+ },
+ "hero": {
+ "themeLightLabel": "رفتن به حالت روشن",
+ "themeDarkLabel": "رفتن به حالت تاریک",
+ "languageButtonLabel": "انتخاب زبان",
+ "languageMenuLabel": "زبانها",
+ "englishLabel": "English",
+ "persianLabel": "فارسی",
+ "blog": "وبلاگ",
+ "blogLabel": "وبلاگ، بهزودی",
+ "courses": "دورهها",
+ "coursesLabel": "دورهها، بهزودی",
+ "comingSoon": "بهزودی",
+ "contact": "ارتباط با من",
+ "contactLabel": "ارسال پیام در لینکدین"
+ },
+ "about": {
+ "journeyLabel": "مسیر حرفهای"
+ },
+ "projects": {
+ "previousLabel": "پروژههای قبلی",
+ "nextLabel": "پروژههای بعدی",
+ "carouselLabel": "فهرست افقی پروژهها",
+ "carouselRoleDescription": "گردونه",
+ "scrollbarLabel": "موقعیت در فهرست پروژهها",
+ "viewCaseStudy": "دیدن مطالعه موردی",
+ "liveProjectLabel": "باز کردن نسخه زنده {title} در زبانه جدید",
+ "drawerLabel": "مطالعه موردی پروژه",
+ "closeCaseStudy": "بستن مطالعه موردی",
+ "shareLabel": "اشتراکگذاری",
+ "shareCaseStudy": "اشتراکگذاری این مطالعه موردی",
+ "shareDialogTitle": "اشتراکگذاری مطالعه موردی",
+ "shareDialogDescription": "این پروژه را برای دیگران بفرستید یا پیوند مستقیم آن را کپی کنید.",
+ "closeShareDialog": "بستن گزینههای اشتراکگذاری",
+ "shareTelegram": "تلگرام",
+ "shareWhatsApp": "واتساپ",
+ "shareLinkedIn": "لینکدین",
+ "shareEmail": "ایمیل",
+ "copyLink": "کپی پیوند",
+ "linkCopied": "پیوند کپی شد",
+ "copyFailed": "کپی پیوند ممکن نشد",
+ "roleLabel": "نقش من",
+ "usersLabel": "کاربران",
+ "statusLabel": "وضعیت",
+ "teamLabel": "تیم",
+ "galleryLabel": "تصاویر محصول",
+ "openGalleryImage": "باز کردن تصویر در نمای تمامصفحه: {caption}",
+ "selectGalleryImage": "نمایش تصویر {index}: {caption}",
+ "imageViewerLabel": "گالری تمامصفحه پروژه",
+ "closeImageViewer": "بستن تصویر تمامصفحه",
+ "previousImage": "تصویر قبلی",
+ "nextImage": "تصویر بعدی",
+ "imageCounter": "{current} از {total}",
+ "challengeLabel": "چالش",
+ "ownershipLabel": "مسئولیتهای من",
+ "decisionsLabel": "تصمیمهای کلیدی",
+ "outcomesLabel": "نتیجهها",
+ "skillsLabel": "مهارتها",
+ "toolsLabel": "ابزارها",
+ "constraintsLabel": "محدودیتها",
+ "metricsLabel": "نتیجههای عددی",
+ "lessonsLabel": "آموختهها",
+ "previousCaseStudy": "پروژه قبلی",
+ "nextCaseStudy": "پروژه بعدی",
+ "decisionLabels": {
+ "product": "محصول",
+ "engineering": "مهندسی",
+ "design": "طراحی"
+ }
+ },
+ "experience": {
+ "logoAlt": "نشان {company}"
+ },
+ "skills": {
+ "skillsLabel": "مهارتها",
+ "toolsLabel": "ابزارها",
+ "filterLabel": "انتخاب حوزه مهارت"
+ },
+ "education": {},
+ "milestones": {
+ "viewCertificate": "دیدن گواهی",
+ "viewCertificateLabel": "دیدن گواهی {title}"
+ },
+ "footer": {
+ "emailLabel": "ارسال ایمیل",
+ "contactsLabel": "راههای ارتباطی و شبکههای اجتماعی",
+ "sceneLabel": "نمای تعاملی سهبعدی راههای ارتباطی",
+ "phone": "تلفن",
+ "linkedin": "لینکدین",
+ "github": "گیتهاب",
+ "telegram": "تلگرام"
+ }
+}
diff --git a/src/content/messages.ts b/src/content/messages.ts
new file mode 100644
index 0000000..0ca31dd
--- /dev/null
+++ b/src/content/messages.ts
@@ -0,0 +1,22 @@
+import english from './messages.en.json';
+import persian from './messages.fa.json';
+export type InterfaceMessages = typeof english;
+export const dictionaries: Record = { en: english, fa: persian };
+export function flattenMessages(value: Record, prefix = ''): Record {
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
+ const path = prefix ? `${prefix}.${key}` : key;
+ return typeof item === 'string' ? [[path, item]] : Object.entries(flattenMessages(item as Record, path));
+ }));
+}
+export const messageDefaults = flattenMessages(english);
+export function resolveMessages(locale: string, overrides: Record): InterfaceMessages {
+ const result = structuredClone(dictionaries[locale] ?? english);
+ for (const [path, value] of Object.entries(overrides)) {
+ if (!Object.hasOwn(messageDefaults, path) || !value) continue;
+ const [section, field, child] = path.split('.');
+ const group = result[section as keyof InterfaceMessages] as Record;
+ if (child) (group[field] as Record)[child] = value;
+ else group[field] = value;
+ }
+ return result;
+}
diff --git a/src/content/samples.ts b/src/content/samples.ts
new file mode 100644
index 0000000..05db250
--- /dev/null
+++ b/src/content/samples.ts
@@ -0,0 +1,21 @@
+import type { EntryKind, Entry, Section, Settings, SectionKey } from './contracts';
+export const sampleImage = { src: '/images/portfolio-placeholder.svg', width: 640, height: 400 };
+export const entrySamples = {
+ projects: { title: 'A meaningful project', liveUrl: '', date: '2026', description: 'Describe the problem you solved and the people you helped.', tags: ['Design', 'Development'], image: sampleImage,
+ caseStudy: { introduction: 'Share the story behind your work.', role: 'Designer and developer', targetUsers: 'Your audience', status: 'Complete', challenge: 'What needed to change?', ownership: [], decisions: [], outcomes: [], skills: [], tools: [], team: '', constraints: [], metrics: [], lessons: [], gallery: [] } },
+ experience: { role: 'Your role', company: 'Organization', date: '2024 – Present', location: 'Remote', logo: sampleImage, isCurrent: true, summary: 'Describe your contribution.', bullets: [] },
+ education: { degree: 'Your qualification', institution: 'Institution', date: '2024', description: 'What did you learn?', logo: sampleImage },
+ milestones: { title: 'A milestone worth sharing', issuer: 'Organization', date: '2026', description: 'Describe your achievement.', image: sampleImage, link: '' },
+ skills: { label: 'My expertise', skills: [{ name: 'Problem solving', level: 4 }], keywords: ['Collaboration'], tools: [] },
+} satisfies Record>;
+export const sectionSamples = {
+ hero: { sectionLabel: 'Introduction', titleLabel: 'Your name — portfolio', greeting: 'Hello, I’m', name: 'Your Name', subtitle: 'I make thoughtful things for people.', avatarAlt: 'Profile image' },
+ about: { eyebrow: 'My journey', title: 'A little about me', description: 'Tell visitors what matters to you and how you approach your work.', steps: [{ title: 'Where it began', description: 'Share a chapter in your journey.' }] },
+ projects: { eyebrow: 'Selected work', title: 'Things I have built' },
+ experience: { title: 'Where I have contributed' }, skills: { eyebrow: 'My toolkit', title: 'Skills and tools' },
+ education: { title: 'What I have learned' }, milestones: { eyebrow: 'Along the way', title: 'Milestones' },
+ footer: { title: 'Let’s make something meaningful.', subtitle: 'Get in touch to discuss your next project.' },
+} satisfies Record>;
+export const sampleSettings: Settings = { defaultLanguage: 'en', avatar: sampleImage, contactUrl: 'mailto:hello@example.com', blogUrl: '', coursesUrl: '', email: 'hello@example.com', contacts: [], metadata: { en: { title: 'Your Name — Portfolio', description: 'A selection of my work and experience.', keywords: [] } } };
+export function sampleEntries(): Entry[] { return Object.entries(entrySamples).map(([kind, data], position) => ({ id: `sample-${kind}`, kind: kind as EntryKind, position, visible: true, shared: data, translations: {} })); }
+export function sampleSections(): Section[] { return Object.entries(sectionSamples).map(([key, data]) => ({ key: key as SectionKey, visible: true, translations: { en: data } })); }