/* global React, ReactDOM */
const { useState, useEffect, useRef, useCallback } = React;
const D = window.PORTFOLIO_DATA;
const C = D.copy; // Customizer copy — edit via Appearance → Customize → Kalligeris Copy
// Parse intrinsic dims from a URL: e.g. "-1024x683.jpg" or "-683x1024.jpg".
// Falls back to orientation flag when no dims present (e.g. "-scaled.jpg").
function aspectFor(item) {
const m = item.src && item.src.match(/[-_](\d{2,4})x(\d{2,4})\.(jpe?g|png|webp)/i);
if (m) return `${m[1]} / ${m[2]}`;
if (item.o === 'p') return '2 / 3';
if (item.o === 's') return '1 / 1';
return '3 / 2';
}
// ---------- Tweaks (in-design) ----------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "cinema",
"layout": "mosaic",
"cursor": false,
"marquee": true,
"accent": "#C2884A"
} /*EDITMODE-END*/;
function postEdit(edits) {
try {window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');} catch (e) {}
}
function useTweaks() {
const [t, setT] = useState(TWEAK_DEFAULTS);
const set = (k, v) => {setT((p) => ({ ...p, [k]: v }));postEdit({ [k]: v });};
return [t, set];
}
// ---------- Custom cursor ----------
function Cursor({ enabled }) {
const dot = useRef(null);
const ring = useRef(null);
const [label, setLabel] = useState('');
useEffect(() => {
if (!enabled) return;
let mx = -100,my = -100,rx = -100,ry = -100,raf;
const tick = () => {
rx += (mx - rx) * 0.18;ry += (my - ry) * 0.18;
if (dot.current) dot.current.style.transform = `translate(${mx}px,${my}px)`;
if (ring.current) ring.current.style.transform = `translate(${rx}px,${ry}px)`;
raf = requestAnimationFrame(tick);
};
const onMove = (e) => {mx = e.clientX;my = e.clientY;};
const onOver = (e) => {
const el = e.target.closest('[data-cursor]');
const v = el ? el.getAttribute('data-cursor') : '';
setLabel(v || '');
document.documentElement.classList.toggle('cursor-hover', !!v);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseover', onOver);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseover', onOver);
};
}, [enabled]);
if (!enabled) return null;
return (
<>
{label}
>
);
}
// ---------- Top nav ----------
function Nav({ now, onJump }) {
const sections = [
...D.sections.map((s) => ({ id: s.slug, label: s.kicker || s.title })),
{ id: 'studio', label: 'Studio' },
{ id: 'contact', label: 'Contact' },
];
return (
{C.brand}
— {C.tagline}
{now.toUpperCase()}
);
}
// ---------- Marquee ----------
function Marquee({ show }) {
if (!show) return null;
const items = C.marquee.split(',').map((s) => s.trim());
const row = items.concat(items, items);
return (
{row.map((x, i) =>
{x}
)}
);
}
// ---------- Hero ----------
function Hero({ onJump }) {
// heroR2 is rendered as plain text — the italic & is a design element kept in markup
return (
(01) — Index
{C.heroVolume}
{C.heroR1}
{C.heroR2}
{C.heroR3}
{C.heroR4}
{C.heroFeaturingLabel}
{D.sections.map((s, i) => {
const count = s.kind === 'projects'
? (s.projects || []).reduce((a, p) => a + (p.images || []).length, 0)
: (s.items || []).length;
const label = s.kind === 'motion' ? 'motion' : s.kind === 'projects' ? 'spaces' : 'stills';
return `${count} ${label}`;
}).join(' · ')}
{C.heroRecentLabel}
{C.heroRecent}
onJump(D.sections[0]?.slug || 'motion')} data-cursor="ENTER">
{C.heroCta}
{D.heroPicks.map((src, i) =>
)}
STK / 25
{C.tagline}
);
}
// ---------- Section header ----------
function SectionHead({ idx, kicker, title, count, sub }) {
return (
);
}
// ---------- Motion ----------
function MotionGrid({ items, onPlay, layout }) {
const cls = layout === 'index' ? 'motion-grid index' : layout === 'stack' ? 'motion-grid stack' : 'motion-grid';
return (
{items.map((v, i) =>
onPlay(v)} data-cursor="PLAY">
{e.currentTarget.src = `https://img.youtube.com/vi/${v.id}/hqdefault.jpg`;}} />
№ {String(i + 1).padStart(2, '0')}
{v.title} · {v.year}
{v.tag} — {v.subtitle}
)}
);
}
// ---------- Food masonry ----------
function FoodGrid({ slug, items, onView, layout }) {
return (
{items.map((it, i) =>
onView(slug, i)} data-cursor="VIEW">
№ {String(i + 1).padStart(2, '0')}
)}
);
}
// ---------- Interiors ----------
function InteriorsBlock({ projects, onView, layout }) {
return (
{projects.map((proj, p) =>
Project {String(p + 1).padStart(2, '0')} / {String(projects.length).padStart(2, '0')}
{proj.location} · {proj.year}
{proj.name}
{proj.blurb}
{proj.images.map((im, i) =>
onView(proj.slug, i)} data-cursor="VIEW">
)}
)}
);
}
// ---------- Lightbox ----------
function Lightbox({ open, list, index, onClose, onPrev, onNext }) {
useEffect(() => {
if (!open) return;
const k = (e) => {
if (e.key === 'Escape') onClose();
else if (e.key === 'ArrowLeft') onPrev();
else if (e.key === 'ArrowRight') onNext();
};
window.addEventListener('keydown', k);
document.body.style.overflow = 'hidden';
return () => {window.removeEventListener('keydown', k);document.body.style.overflow = '';};
}, [open, onClose, onPrev, onNext]);
if (!open || !list) return null;
const item = list[index];
return (
{String(index + 1).padStart(2, '0')} / {String(list.length).padStart(2, '0')}
{e.stopPropagation();onClose();}} data-cursor="CLOSE">Close ×
{e.stopPropagation();onPrev();}} data-cursor="PREV">←
e.stopPropagation()} data-cursor="" />
{e.stopPropagation();onNext();}} data-cursor="NEXT">→
);
}
// ---------- Video modal ----------
function VideoModal({ video, onClose }) {
useEffect(() => {
if (!video) return;
const k = (e) => {if (e.key === 'Escape') onClose();};
window.addEventListener('keydown', k);
document.body.style.overflow = 'hidden';
return () => {window.removeEventListener('keydown', k);document.body.style.overflow = '';};
}, [video, onClose]);
if (!video) return null;
return (
e.stopPropagation()}>
{video.title} — {video.subtitle}
Close ×
VIDEO
);
}
// ---------- Studio ----------
function Studio({ onJump }) {
return (
(04) — Studio
{C.studioTitle}
01 {C.studioItem1}
02 {C.studioItem2}
03 {C.studioItem3}
04 {C.studioItem4}
{C.studioImage
?
: <>
Studio photo drop a 4:5 image here · ~1600×2000px
>
}
{C.studioStat1} {C.studioStat1Label}
{C.studioStat2} {C.studioStat2Label}
{C.studioStat3} {C.studioStat3Label}
);
}
// ---------- Contact ----------
function Contact() {
return (
);
}
// ---------- Tweaks panel ----------
function TweaksUI({ t, setT, visible, onClose }) {
if (!visible) return null;
const themes = [
{ id: 'cinema', label: 'Cinema' },
{ id: 'editorial',label: 'Editorial' },
{ id: 'daylight', label: 'Daylight' },
];
const layouts = [
{ id: 'mosaic', label: 'Mosaic' },
{ id: 'index', label: 'Index' },
{ id: 'stack', label: 'Stack' },
];
const accents = ['#C2884A', '#7C5C3E', '#D9C7A0', '#A33D2A', '#E8E2D2'];
return (
Tweaks
×
Theme
{themes.map((x) =>
setT('theme', x.id)} data-cursor="">{x.label}
)}
Layout
{layouts.map((x) =>
setT('layout', x.id)} data-cursor="">{x.label}
)}
Accent
{accents.map((c) =>
setT('accent', c)} data-cursor="" />
)}
Cursor
setT('cursor', !t.cursor)} data-cursor="">
{t.cursor ? 'Custom' : 'Native'}
Marquee
setT('marquee', !t.marquee)} data-cursor="">
{t.marquee ? 'On' : 'Off'}
);
}
// ---------- App ----------
function App() {
const [t, setT] = useTweaks();
const [video, setVideo] = useState(null);
const [lb, setLb] = useState(null);
const [now, setNow] = useState('');
const [tweakOpen, setTweakOpen] = useState(false);
useEffect(() => {
const fmt = () => {
const d = new Date();
const time = d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Athens' });
setNow(`Athens ${time}`);
};
fmt();
const i = setInterval(fmt, 30000);
return () => clearInterval(i);
}, []);
useEffect(() => {
const onMsg = (e) => {
if (!e.data || typeof e.data !== 'object') return;
if (e.data.type === '__activate_edit_mode') setTweakOpen(true);
if (e.data.type === '__deactivate_edit_mode') setTweakOpen(false);
};
window.addEventListener('message', onMsg);
try {window.parent.postMessage({ type: '__edit_mode_available' }, '*');} catch (e) {}
return () => window.removeEventListener('message', onMsg);
}, []);
useEffect(() => {
const root = document.documentElement;
root.setAttribute('data-theme', t.theme);
root.style.setProperty('--accent', t.accent);
}, [t.theme, t.accent]);
const jump = (id) => {
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const openView = useCallback((key, idx) => {
let list;
for (const sec of D.sections) {
if (sec.kind === 'stills' && sec.slug === key) list = sec.items;
if (sec.kind === 'projects') {
const p = (sec.projects || []).find((p) => p.slug === key);
if (p) list = p.images;
}
}
if (list) setLb({ key, list, index: idx });
}, []);
const closeLb = () => setLb(null);
const prev = () => setLb((s) => s ? { ...s, index: (s.index - 1 + s.list.length) % s.list.length } : s);
const next = () => setLb((s) => s ? { ...s, index: (s.index + 1) % s.list.length } : s);
return (
<>
{D.sections.map((sec, i) => {
const idx = String(i + 1).padStart(2, '0');
const count = sec.kind === 'projects'
? (sec.projects || []).reduce((a, p) => a + (p.images || []).length, 0)
: (sec.items || []).length;
const sub = sec.sub || (sec.kind === 'projects' ? `${(sec.projects || []).length} projects` : undefined);
return (
{sec.kind === 'motion' && }
{sec.kind === 'stills' && }
{sec.kind === 'projects' && }
);
})}
setVideo(null)} />
{
setTweakOpen(false);
try {window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');} catch (e) {}
}} />
>
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );