// Journal.jsx — Editorial blog / inspiration
//
// Articles are now SERVER-PERSISTED. Source of truth is
// /api/journal (published only) and /api/admin/journal (all
// including drafts, admin-gated).
//
// The module-level `articlesStore` mirrors the same pattern used
// for the finishes list — a tiny pub-sub store so any component
// in the app (Journal here, Home teasers, Phone listings) can
// subscribe via `useArticles()` and re-render when the studio
// edits a post.
//
// `ARTICLES` (the export name) is kept as a Proxy that reads the
// live list, so anywhere in the codebase that still imports
// `window.ARTICLES` keeps working transparently.
const { useState, useEffect } = React;

// Seed = inline fallback so the journal page isn't empty during the
// first ~50 ms before /api/journal resolves. Kept short — the real
// content comes from the server.
const ARTICLES_SEED = [
  {
    id: 1,
    title: 'Outdoor Tiles: A Buyer\'s Guide',
    category: 'Outdoor',
    date: 'May 2026',
    read: '6 min read',
    hero:  '/assets/generated/lifestyle-outdoor-patio.jpg',   // KEPT OLD IMAGE per request
    thumb: '/assets/generated/lifestyle-outdoor-patio.jpg',
    intro: 'Frost rating, slip class, format, finish — what actually matters when you\'re specifying a tile that has to live with the London weather.',
    body: `An outdoor tile gets one job — survive — and four conditions to survive: frost, water, sunlight, and foot traffic. Get any of them wrong and you\'re re-laying the patio in three winters.

Start with frost rating. In London you want a tile rated to ISO 10545-12 with a verified water absorption below 0.5%. Anything more porous will hold water, freeze, and crack. Most reputable porcelain hits this bar; many cheaper "outdoor" terracotta tiles do not.

Slip class matters next. R11 is the minimum for a wet patio; R12 if there\'s any chance of leaves, algae, or pool spray. Showrooms underweight this — until someone slips.

Format and finish are the easy decisions. Larger formats (60×60 and up) read more architectural and have fewer joints to maintain. A natural or honed finish reads as stone; lapped finishes are for indoors. And colour: the warmer the surface, the cooler it stays underfoot in summer.`,
  },
  {
    id: 2,
    title: 'Living, Layered',
    category: 'Floor · Stone',
    date: 'May 2026',
    read: '4 min read',
    hero:  '/assets/journal/Dakota-ContemporaryStone_1.jpg',
    thumb: '/assets/journal/Dakota-ContemporaryStone_1.jpg',
    intro: 'How a single concrete-look porcelain floor lets a modern living room flex between calm grey weekdays and emerald-velvet evenings.',
    body: `The trick to a modern living room is committing to one floor and letting the textiles carry the season. Concrete-look porcelain — Dakota, in this case — gives you that floor.

It reads as a single unbroken plane in pale neutral grey. There\'s enough variation between tiles that it never goes sterile, but the variation is small enough that a green velvet armchair can drop in without competing.

The format choice matters: 60×60 cm at minimum, and a tight joint (1.5 mm in a matching grey grout). Anything smaller starts to look like a contract floor.

In a 50 m² living-dining room you\'re looking at roughly 18 cartons. Allow another 3% for cuts. Most clients are surprised that the install is the cheap part of this composition — the floor itself is the investment, and it stays put for thirty years.`,
  },
  {
    id: 3,
    title: 'Wood-Look Floors: A Field Guide',
    category: 'Floor · Wood',
    date: 'April 2026',
    read: '5 min read',
    hero:  '/assets/journal/DURSTONE_MICHIGAN-OAK-1.webp',
    thumb: '/assets/journal/DURSTONE_MICHIGAN-OAK-1.webp',
    intro: 'Why porcelain in oak grain is the floor that finally lets you put real warmth where real wood was never going to survive.',
    body: `Real wood and bathrooms don\'t mix. Real wood and underfloor heating fight. Real wood and dogs argue. Wood-look porcelain solves all three without giving up the visual warmth.

The new generation — Michigan Oak, our current staple — uses high-resolution digital print across each plank, so no two pieces in a carton repeat. From four feet away, your eye reads it as oak; from the wear surface, it\'s indestructible porcelain.

Plank format matters. 25×100 or 25×150 reads as floorboards; 20×60 reads as tile pretending to be wood (and not very well). Always specify the long format for residential installs.

Lay direction is the only thing most installers get wrong: planks should run with the long axis of the room, not across it. It elongates the space.`,
  },
  {
    id: 4,
    title: 'Marble\'s Modern Vernacular',
    category: 'Wall · Marble',
    date: 'March 2026',
    read: '5 min read',
    hero:  '/assets/journal/Swan-Durstone_1-1.webp',
    thumb: '/assets/journal/Swan-Durstone_1-1.webp',
    intro: 'White marble has stopped meaning Renaissance and started meaning mid-century. A short reading on how the new generation is using it.',
    body: `For a long time, white marble in interiors meant only one thing — a pastiche of an old-world palazzo. The new generation has rescued it. Used as a single feature wall, polished to a mirror sheen, paired with chrome or matt black, it reads as Mid-century modern, not Pope-era classical.

The Swan series we carry takes its veining cues from Calacatta, but with a tighter, more linear pattern. It books and matches at large format (120×280 cm slab equivalents) so a feature wall can be read as a single composition rather than a tiled grid.

Two things to avoid. One: don\'t mix marble looks. A polished white feature wall plus a marble-look floor confuses the eye. Two: skip the contrasting grout. A 1 mm joint in a near-white tone keeps the marble reading as material, not pattern.`,
  },
  {
    id: 5,
    title: 'The Quiet Kitchen',
    category: 'Kitchen',
    date: 'March 2026',
    read: '5 min read',
    hero:  '/assets/journal/Terramar-ContemporaryStone_4.webp',
    thumb: '/assets/journal/Terramar-ContemporaryStone_4.webp',
    intro: 'Cream stone-look porcelain, oak cabinetry, exposed beams — a study in the kitchen that disappears so the cooking shows.',
    body: `A loud kitchen is a tired kitchen by year three. The trick to a kitchen you still love at year ten is to build it almost monochromatically and let one element carry the energy.

The Terramar floor in this composition is a warm cream stone-look porcelain at 80×80 — generous, no aggressive grain. The walls match it in tone but not exactly in finish, which gives the room subtle depth without strong contrast. The oak cabinetry is plain; the brass tap is the only metallic note.

What\'s missing is what makes the room work: there are no patterned surfaces fighting for attention. The eye finds nothing to argue with, so it relaxes. Then the food, the people, and the daylight do the work.

This kind of kitchen costs less to specify than a maximalist one — and ages forward instead of dating.`,
  },
  {
    id: 6,
    title: 'From the London Studio, by Hand',
    category: 'Studio',
    date: 'February 2026',
    read: '7 min read',
    hero:  '/assets/journal/Descargar-Serie-Bn.webp',
    thumb: '/assets/journal/Descargar-Serie-Bn.webp',
    intro: 'A morning in the warehouse where every surface arrives, gets pulled, gets photographed, and gets sent to one of you.',
    body: `Most tile websites show you a render. Ours shows you the actual sample we pulled from the actual carton in our actual warehouse — which is what this morning at the studio was about.

Every series we list arrives in a pallet with two or three identifying pieces. Those pieces get pulled, set against a neutral wall, photographed under our standard lighting setup, and added to the catalogue. If a batch doesn\'t match the photograph, we re-shoot — we don\'t adjust the photo.

The black-and-white image is from one of those mornings. The tile being held is a mid-grey stone-look from a series we carry. Every tile we ship has been through this hands-on inspection.

It\'s not how the big tile retailers work. It is how a studio works. We think it\'s why the tile that arrives at your address looks like the tile you ordered.`,
  },
];

// ─── articlesStore — module-level pub/sub mirror of /api/journal ──────
const articlesStore = { list: [...ARTICLES_SEED], loaded: false, listeners: new Set() };
function setArticlesGlobal(next) {
  articlesStore.list = Array.isArray(next) ? next : [];
  articlesStore.loaded = true;
  for (const fn of articlesStore.listeners) fn();
}
async function refreshArticles() {
  try {
    const r = await fetch('/api/journal');
    const d = await r.json();
    if (Array.isArray(d.articles)) setArticlesGlobal(d.articles);
  } catch {}
}
// Kick off the first fetch as soon as this script loads — by the
// time React mounts, articles are usually already in the store.
refreshArticles();

function useArticles() {
  const [, tick] = useState(0);
  useEffect(() => {
    const fn = () => tick(n => n + 1);
    articlesStore.listeners.add(fn);
    return () => { articlesStore.listeners.delete(fn); };
  }, []);
  return articlesStore.list;
}

// Live Proxy export — keeps `window.ARTICLES.length`, `window.ARTICLES.map(...)`,
// `window.ARTICLES.find(...)`, etc. working in every other file that
// imports it without a rename pass.
const ARTICLES = new Proxy({}, {
  get(_t, prop) {
    const live = articlesStore.list;
    if (prop === 'length') return live.length;
    if (prop === Symbol.iterator) return live[Symbol.iterator].bind(live);
    if (typeof prop === 'string' && /^\d+$/.test(prop)) return live[Number(prop)];
    const v = live[prop];
    return typeof v === 'function' ? v.bind(live) : v;
  },
});

function ArticleView({ article, onBack }) {
  useReveal();
  return (
    <div style={{ paddingTop: 'var(--nav-h)' }}>
      <div style={{ height: '65vh', overflow: 'hidden', position: 'relative', background: 'var(--cream-deep)' }}>
        <img src={article.hero} alt={article.title}
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
          onError={e => e.target.style.display='none'}
        />
        <div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.3)' }}/>
      </div>
      <div style={{ maxWidth: '760px', margin: '0 auto', padding: 'clamp(40px, 8vw, 72px) clamp(20px, 5vw, 40px)' }}>
        <button onClick={onBack} style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--sans)', fontSize: '11px', color: 'var(--dark-mid)', letterSpacing: '0.12em', textTransform: 'uppercase', padding: 0, marginBottom: '40px' }}>
          ← Journal
        </button>
        <p className="t-label reveal" style={{ color: 'var(--terracotta)', marginBottom: '16px' }}>{article.category} · {article.date} · {article.read}</p>
        <h1 className="t-display reveal reveal-delay-1" style={{ fontSize: 'clamp(36px, 4vw, 60px)', marginBottom: '24px' }}>{article.title}</h1>
        <div className="divider reveal reveal-delay-2"/>
        <p className="reveal reveal-delay-2" style={{ fontFamily: 'var(--serif)', fontSize: '22px', fontWeight: 300, lineHeight: 1.65, color: 'var(--dark-mid)', margin: '32px 0 40px', fontStyle: 'italic' }}>
          {article.intro}
        </p>
        {article.body.split('\n\n').map((para, i) => (
          <p key={i} className="reveal" style={{ fontFamily: 'var(--sans)', fontSize: '16px', fontWeight: 300, lineHeight: 1.85, color: 'var(--dark-mid)', marginBottom: '24px' }}>
            {para}
          </p>
        ))}
      </div>
    </div>
  );
}

function Journal({ navigate, pageData }) {
  // Article-open state now lives in the App's history, not internal
  // useState — so the browser back button returns to the journal list
  // instead of skipping straight back to whatever page came before.
  // Click an article  → navigate('journal', { articleId }) — pushes state.
  // Click "← Journal" → navigate('back')  — pops back to the list.
  // Browser back     → App's popstate handler restores pageData=null,
  //                     this component re-renders with no active article.
  const articles = useArticles();
  const activeArticle = (pageData && pageData.articleId)
    ? articles.find(a => a.id === pageData.articleId)
    : null;
  useReveal();

  if (activeArticle) return <ArticleView article={activeArticle} onBack={() => navigate('back')}/>;

  if (articles.length === 0) {
    return (
      <div style={{ paddingTop: 'var(--nav-h)', padding: '120px 40px', textAlign: 'center' }}>
        <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '14px' }}>Journal</p>
        <p style={{ fontFamily: 'var(--serif)', fontSize: '22px', fontWeight: 300, color: 'var(--dark-mid)' }}>
          No articles yet — check back soon.
        </p>
      </div>
    );
  }

  const [featured, ...rest] = articles;

  return (
    <div style={{ paddingTop: 'var(--nav-h)' }}>
      <div style={{ padding: 'clamp(56px, 9vw, 80px) clamp(20px, 5vw, 40px) clamp(40px, 7vw, 64px)', borderBottom: '1px solid var(--cream-deep)' }}>
        <p className="t-label reveal" style={{ color: 'var(--terracotta)', marginBottom: '14px' }}>Journal</p>
        <h1 className="t-display reveal reveal-delay-1" style={{ fontSize: 'clamp(42px, 5vw, 76px)' }}>
          Ideas, rooms<br /><em>&amp; material stories</em>
        </h1>
      </div>

      {/* Featured article */}
      <div className="journal-featured" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', cursor: 'pointer', borderBottom: '1px solid var(--cream-deep)' }}
        onClick={() => navigate('journal', { articleId: featured.id })}>
        <div style={{ overflow: 'hidden', background: 'var(--cream-deep)', aspectRatio: '4/3' }}>
          <img src={featured.hero} alt={featured.title}
            style={{ width: '100%', height: '100%', objectFit: 'cover', transition: 'transform 0.8s var(--ease-out)' }}
            onMouseEnter={e => e.target.style.transform = 'scale(1.04)'}
            onMouseLeave={e => e.target.style.transform = 'scale(1)'}
            onError={e => e.target.style.display='none'}
          />
        </div>
        <div style={{ padding: 'clamp(40px, 7vw, 80px) clamp(24px, 6vw, 64px)', display: 'flex', flexDirection: 'column', justifyContent: 'center', background: 'var(--cream-mid)' }}>
          <p className="t-label reveal" style={{ color: 'var(--terracotta)', marginBottom: '12px' }}>Featured · {featured.category}</p>
          <h2 className="t-display reveal reveal-delay-1" style={{ fontSize: 'clamp(30px, 3vw, 48px)', marginBottom: '20px' }}>{featured.title}</h2>
          <p className="reveal reveal-delay-2" style={{ fontFamily: 'var(--sans)', fontWeight: 300, fontSize: '15px', color: 'var(--dark-mid)', lineHeight: 1.75, marginBottom: '32px', maxWidth: '420px' }}>{featured.intro}</p>
          <span style={{ fontFamily: 'var(--sans)', fontSize: '11px', letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--dark)' }}>
            Read · {featured.read} →
          </span>
        </div>
      </div>

      {/* Article grid */}
      <div style={{ padding: 'clamp(40px, 8vw, 80px) clamp(20px, 5vw, 40px)' }}>
        <div className="journal-list-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '48px 32px' }}>
          {rest.map((a, i) => (
            <article key={a.id} className="reveal" style={{ transitionDelay: `${i*0.1}s`, cursor: 'pointer' }}
              onClick={() => navigate('journal', { articleId: a.id })}>
              <div style={{ aspectRatio: '4/3', overflow: 'hidden', background: 'var(--cream-deep)', marginBottom: '24px' }}>
                <img src={a.thumb} alt={a.title}
                  style={{ width: '100%', height: '100%', objectFit: 'cover', transition: 'transform 0.7s var(--ease-out)' }}
                  onMouseEnter={e => e.target.style.transform = 'scale(1.05)'}
                  onMouseLeave={e => e.target.style.transform = 'scale(1)'}
                  onError={e => e.target.style.display='none'}
                />
              </div>
              <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '10px' }}>{a.category} · {a.read}</p>
              <h3 style={{ fontFamily: 'var(--serif)', fontSize: '26px', fontWeight: 400, lineHeight: 1.2, marginBottom: '10px' }}>{a.title}</h3>
              <p style={{ fontFamily: 'var(--sans)', fontSize: '13px', fontWeight: 300, color: 'var(--dark-mid)', lineHeight: 1.7 }}>{a.intro.slice(0, 90)}…</p>
            </article>
          ))}
        </div>
      </div>
      <style>{`
        @media (max-width: 760px) {
          .journal-featured { grid-template-columns: 1fr !important; }
          .journal-list-grid { grid-template-columns: repeat(2, 1fr) !important; gap: 36px 20px !important; }
        }
        @media (max-width: 520px) {
          .journal-list-grid { grid-template-columns: 1fr !important; gap: 32px 0 !important; }
        }
      `}</style>
    </div>
  );
}

Object.assign(window, {
  Journal, ARTICLES,
  // Expose the store + hook so the Admin Journal panel can trigger a
  // refresh after an edit, and so Home/Phone teasers can re-render.
  // QW_articlesStore is the raw pub-sub primitive — consumers that
  // need a hook-safe subscription at the top of their component
  // (Home.jsx) use it directly via useState + useEffect rather than
  // calling useArticles() from a conditional / nested position.
  useArticles, refreshArticles, setArticlesGlobal,
  QW_articlesStore: articlesStore,
});

