{"id":9938,"date":"2025-11-25T14:22:02","date_gmt":"2025-11-25T14:22:02","guid":{"rendered":"https:\/\/www.animcloud.fr\/?page_id=9938"},"modified":"2026-02-15T11:21:03","modified_gmt":"2026-02-15T11:21:03","slug":"planning","status":"publish","type":"page","link":"https:\/\/www.animcloud.fr\/en\/planning\/","title":{"rendered":"Planning"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"utf-8\" \/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" \/>\n<title>Planning Animcloud<\/title>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@300;400;600;700&#038;display=swap\" rel=\"stylesheet\">\n<style>\n  :root{\n    --bg: #f3f6f9;\n    --card: #ffffff;\n    --accent: #ff6b4a;\n    --muted: #6b7280;\n    --max-width: 1200px;\n    --radius: 12px;\n  }\n  *{box-sizing:border-box}\n  html,body{height:100%}\n  body{\n    margin:0;\n    font-family:Inter, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial;\n    background:var(--bg);\n    color:#0b1220;\n    padding:18px;\n  }\n\n  .page{max-width:var(--max-width); margin:0 auto; display:flex; flex-direction:column; gap:18px;}\n  header{display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;}\n  .brand{display:flex; gap:12px; align-items:center;}\n  .logo{\n    width:44px; height:44px; border-radius:10px; background:linear-gradient(135deg,#2b4d8a,#111827); display:flex; align-items:center; justify-content:center;\n    color:white; font-weight:800; font-size:1.05rem;\n  }\n  h1{font-size:1.1rem; margin:0;}\n  p.lead{margin:2px 0 0 0; color:var(--muted); font-size:.95rem;}\n\n  \/* Style du s\u00e9lecteur *\/\n  .session-selector select {\n    padding: .55rem;\n    border-radius: 8px;\n    border: 1px solid #ccc;\n    font-family: inherit;\n    background-color: white;\n    font-weight: 600;\n    color: #333;\n    cursor: pointer;\n    min-width: 200px;\n  }\n\n  .actions{display:flex; gap:10px; align-items:center; flex-wrap:wrap;}\n  .btn{\n    background:var(--accent);\n    color:white;\n    padding:.55rem .85rem;\n    border-radius:10px;\n    font-weight:700;\n    text-decoration:none;\n    display:inline-flex;\n    gap:.6rem;\n    align-items:center;\n    transition: opacity 0.2s;\n    font-size: 0.9rem;\n  }\n  .btn:hover{opacity: 0.9;}\n  \n  .embed-wrap{\n    width:100%;\n    border-radius:var(--radius);\n    overflow:hidden;\n    background:var(--card);\n    box-shadow:0 12px 30px rgba(6,12,24,0.08);\n    position:relative;\n    transition: opacity 0.3s ease;\n    min-height: 600px;\n  }\n  \/* Animation de chargement *\/\n  .embed-wrap.loading { opacity: 0.5; pointer-events: none; }\n\n  .embed-frame{\n    width:100%;\n    height:800px; \/* Hauteur par d\u00e9faut *\/\n    border:0;\n    display:block;\n  }\n\n  @media(max-width: 600px){\n    .embed-frame{ height: 600px; }\n    .brand h1 { font-size: 1rem; }\n    .brand .logo { width: 36px; height: 36px; font-size: 0.9rem; }\n  }\n\n  .meta{\n    display:flex;\n    justify-content:center;\n    margin-top: 10px;\n  }\n  .note{color:var(--muted); font-size:.85rem;}\n<\/style>\n<\/head>\n<body>\n  <div class=\"page\" role=\"main\">\n    <header>\n      <div class=\"brand\">\n        <div class=\"logo\" aria-hidden=\"true\">AC<\/div>\n        <div>\n          <h1 id=\"userTitle\">Planning<\/h1>\n          <p class=\"lead\">Semaine de No\u00ebl<\/p>\n        <\/div>\n      <\/div>\n\n      <div class=\"session-selector\">\n        <select id=\"animatorSelect\">\n          <\/select>\n      <\/div>\n\n      <div class=\"actions\">\n        <a id=\"openSheet\" class=\"btn\" href=\"#\" target=\"_blank\">Ouvrir Plein \u00c9cran<\/a>\n      <\/div>\n    <\/header>\n\n    <section aria-label=\"Planning\" class=\"embed-wrap\" id=\"embedContainer\">\n      <iframe\n        class=\"embed-frame\"\n        id=\"sheetIframe\"\n        title=\"Planning\"\n        src=\"\" \n        loading=\"lazy\"\n        allow=\"clipboard-write; encrypted-media; fullscreen\"\n      ><\/iframe>\n    <\/section>\n\n    <div class=\"meta\">\n      <p class=\"note\">Mise \u00e0 jour automatique en temps r\u00e9el.<\/p>\n    <\/div>\n  <\/div>\n\n<script>\n  \/\/ =================================================================\n  \/\/ \u2699\ufe0f CONFIGURATION (Mise \u00e0 jour avec tes liens)\n  \/\/ =================================================================\n  \n  \/\/ Cl\u00e9 publique extraite de tes iframes\n  const SHEET_KEY = \"2PACX-1vRBgrflmTAyX-dy_-RWGHTYTgIIy6H6nqpnqD1do9srjyWg8I8i8HBwldtxO8PPdN3qE6Wh9MUV1Pey\"; \n\n  \/\/ Mapping Nom -> GID (L'identifiant unique de l'onglet)\n  const ANIMATEURS = {\n    \"Planning G\u00e9n\u00e9ral\": \"545073920\", \/\/ Par d\u00e9faut\n    \"Thibault\": \"1901180931\",\n    \"Tepoe\":    \"492557405\",\n    \"Sacha\":    \"889438457\",\n    \/\/ Ajoute les autres ici quand ils seront g\u00e9n\u00e9r\u00e9s (ex: Julie, Marc...)\n  };\n\n  \/\/ =================================================================\n  \/\/ \ud83d\ude80 LOGIQUE\n  \/\/ =================================================================\n\n  const iframe = document.getElementById('sheetIframe');\n  const select = document.getElementById('animatorSelect');\n  const title = document.getElementById('userTitle');\n  const btn = document.getElementById('openSheet');\n  const container = document.getElementById('embedContainer');\n\n  \/\/ 1. Remplir le menu d\u00e9roulant\n  for (const [name, gid] of Object.entries(ANIMATEURS)) {\n    const option = document.createElement('option');\n    option.value = gid;\n    option.textContent = name;\n    select.appendChild(option);\n  }\n\n  \/\/ 2. Fonction pour charger l'iframe\n  function loadPlanning(gid, name) {\n    container.classList.add('loading');\n    \n    \/\/ Construction URL\n    \/\/ widget=false pour masquer les onglets du bas\n    \/\/ chrome=false pour un look plus clean\n    const url = `https:\/\/docs.google.com\/spreadsheets\/d\/e\/${SHEET_KEY}\/pubhtml?gid=${gid}&single=true&widget=false&headers=false&chrome=false`;\n\n    iframe.src = url;\n    btn.href = url;\n    title.textContent = `Planning \u2014 ${name}`;\n\n    iframe.onload = function() {\n      container.classList.remove('loading');\n    };\n  }\n\n  \/\/ 3. \u00c9couteur sur le menu d\u00e9roulant\n  select.addEventListener('change', (e) => {\n    const gid = e.target.value;\n    const name = e.target.options[e.target.selectedIndex].text;\n    loadPlanning(gid, name);\n  });\n\n  \/\/ 4. Chargement initial (URL ou D\u00e9faut)\n  const urlParams = new URLSearchParams(window.location.search);\n  const userParam = urlParams.get('user');\n\n  if (userParam && ANIMATEURS[userParam]) {\n    \/\/ Si ?user=Thibault dans l'URL\n    select.value = ANIMATEURS[userParam];\n    loadPlanning(ANIMATEURS[userParam], userParam);\n  } else {\n    \/\/ Sinon, on charge le Planning G\u00e9n\u00e9ral par d\u00e9faut\n    const defaultName = \"Planning G\u00e9n\u00e9ral\";\n    select.value = ANIMATEURS[defaultName];\n    loadPlanning(ANIMATEURS[defaultName], defaultName);\n  }\n\n<\/script>\n<\/body>\n<\/html>\n\n\n\n<!doctype html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"utf-8\" \/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n<title>M\u00e9t\u00e9o Val Fr\u00e9jus \u2014 ticker lisible<\/title>\n<style>\n  body { margin:0; font-family:'Inter',sans-serif; padding-bottom:100px; background:#f7fafc; }\n  .weather-banner {\n    position:fixed;\n    bottom:0; left:0; right:0;\n    padding:12px 0;\n    overflow:hidden;\n    z-index:1000;\n    display:flex;\n    align-items:center;\n    font-weight:500;\n    background:linear-gradient(to right,#0ea5a4,#06b6d4);\n    color:white;\n    box-shadow:0 -4px 12px rgba(0,0,0,0.3);\n  }\n  .ticker-viewport { width:100%; overflow:hidden; }\n  .weather-ticker { display:inline-block; white-space:nowrap; transform:translateX(0); will-change:transform; }\n  .weather-item { display:inline-flex; align-items:center; margin-right:16px; padding:6px 10px; border-radius:10px; gap:8px; font-size:14px; box-shadow:1px 1px 5px rgba(0,0,0,0.12); background:rgba(255,255,255,0.06); color:#fff; }\n  .current-weather { font-size:15px; font-weight:600; padding:6px 10px; border-radius:12px; margin-right:16px; display:inline-flex; align-items:center; gap:8px; box-shadow:1px 1px 6px rgba(0,0,0,0.18); background:rgba(255,255,255,0.12); }\n  .weather-icon { width:20px; height:20px; display:inline-block; font-size:18px; }\n\n  \/* pointer cursor to indicate pause on hover (desktop) *\/\n  .ticker-viewport:hover { cursor: pointer; }\n\n  \/* reduced motion *\/\n  @media (prefers-reduced-motion: reduce) {\n    .weather-ticker { animation: none !important; transform: none !important; }\n    .weather-banner { white-space:normal; overflow:auto; }\n  }\n<\/style>\n<\/head>\n<body>\n\n<div class=\"weather-banner\">\n  <div class=\"ticker-viewport\" aria-live=\"polite\">\n    <div id=\"ticker\" class=\"weather-ticker\" tabindex=\"0\">Chargement m\u00e9t\u00e9o Val Fr\u00e9jus&#8230;<\/div>\n  <\/div>\n<\/div>\n\n<script>\n\/* ---------- CONFIG ---------- *\/\n\/\/ Coordonn\u00e9es Val Fr\u00e9jus\nconst LAT = 45.2017, LON = 6.6667;\n\/\/ Temps recommand\u00e9 par item (secondes)\nconst VISIBLE_TIME_PER_ITEM_MOBILE = 2.2;   \/\/ 2.2s par item sur mobile (lisible)\nconst VISIBLE_TIME_PER_ITEM_DESKTOP = 1.4;  \/\/ 1.4s par item sur desktop\n\/\/ min \/ max dur\u00e9e d'une boucle (en secondes)\nconst MIN_DURATION_SEC = 6;\nconst MAX_DURATION_SEC = 60;\n\/\/ breakpoint mobile\nconst MOBILE_BREAKPOINT_PX = 700;\n\/\/ refresh interval\nconst REFRESH_MINUTES = 15;\n\n\/* ---------- HELPERS ---------- *\/\nconst ticker = document.getElementById('ticker');\nconst viewport = document.querySelector('.ticker-viewport');\n\nfunction weatherCodeToIcon(code,hour){\n  const night = hour<6 || hour>=20;\n  if([0,1].includes(code)) return night ? '\ud83c\udf19' : '\u2600\ufe0f';\n  if([2,3].includes(code)) return night ? '\u2601\ufe0f' : '\u26c5';\n  if([45,48].includes(code)) return '\ud83c\udf2b\ufe0f';\n  if([51,53,55,61,63,65].includes(code)) return '\ud83c\udf27\ufe0f';\n  if([71,73,75].includes(code)) return '\u2744\ufe0f';\n  if([95,96].includes(code)) return '\u26c8\ufe0f';\n  return '\u2754';\n}\nfunction weatherCodeToColor(code,hour){\n  const night = hour<6 || hour>=20;\n  if([0,1].includes(code)) return night ? 'rgba(30,30,60,0.45)' : 'rgba(250,204,21,0.25)';\n  if([2,3].includes(code)) return night ? 'rgba(60,60,80,0.45)' : 'rgba(100,116,139,0.18)';\n  if([45,48].includes(code)) return 'rgba(148,163,184,0.18)';\n  if([51,53,55,61,63,65].includes(code)) return 'rgba(59,130,246,0.18)';\n  if([71,73,75].includes(code)) return 'rgba(240,249,255,0.18)';\n  if([95,96].includes(code)) return 'rgba(239,68,68,0.18)';\n  return 'rgba(255,255,255,0.12)';\n}\nfunction formatHour(dateStr){\n  return new Date(dateStr).getHours().toString().padStart(2,'0') + 'h';\n}\n\n\/* ---------- Build ticker content ---------- *\/\nasync function buildTickerContent(){\n  try {\n    const url = `https:\/\/api.open-meteo.com\/v1\/meteofrance?latitude=${LAT}&longitude=${LON}&hourly=temperature_2m,windspeed_10m,precipitation_probability,weathercode&current_weather=true&timezone=auto`;\n    const r = await fetch(url);\n    const data = await r.json();\n    const parts = [];\n\n    \/\/ current\n    const cw = data.current_weather;\n    const hourNow = new Date(cw.time).getHours();\n    parts.push(`<div class=\"current-weather\" style=\"background:${weatherCodeToColor(cw.weathercode,hourNow)}\">\n                  <span class=\"weather-icon\">${weatherCodeToIcon(cw.weathercode,hourNow)}<\/span>\n                  ${formatHour(cw.time)} : ${Math.round(cw.temperature)}\u00b0C \u2022 Vent ${Math.round(cw.windspeed)} km\/h\n                <\/div>`);\n\n    \/\/ 24h forecast\n    for(let i=0;i<24;i++){\n      const temp = Math.round(data.hourly.temperature_2m[i]);\n      const wind = Math.round(data.hourly.windspeed_10m[i]);\n      const rain = data.hourly.precipitation_probability[i] ?? 0;\n      const code = data.hourly.weathercode[i];\n      const hour = formatHour(data.hourly.time[i]);\n      const hnum = new Date(data.hourly.time[i]).getHours();\n      parts.push(`<div class=\"weather-item\" style=\"background:${weatherCodeToColor(code,hnum)}\">\n                    <span class=\"weather-icon\">${weatherCodeToIcon(code,hnum)}<\/span>\n                    ${hour} \u2013 ${temp}\u00b0C \u2022 Vent ${wind} km\/h \u2022 Pluie ${rain}%\n                  <\/div>`);\n    }\n\n    \/\/ duplicate for smooth loop\n    ticker.innerHTML = parts.join('') + parts.join('');\n    \/\/ small delay so DOM updates sizes\n    requestAnimationFrame(() => {\n      setAdaptiveAnimation();\n      attachPauseHandlers(); \/\/ reattach handlers if rebuilt\n    });\n  } catch (err) {\n    console.error(err);\n    ticker.textContent = 'Impossible de r\u00e9cup\u00e9rer la m\u00e9t\u00e9o';\n  }\n}\n\n\/* ---------- Adaptive animation based on item count (readable) ---------- *\/\nlet rafId = null;\nlet pauseState = false;\n\nfunction setAdaptiveAnimation(){\n  \/\/ respect reduced motion\n  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n    ticker.style.transform = 'none';\n    return;\n  }\n\n  \/\/ cancel previous animation\n  if (rafId) cancelAnimationFrame(rafId);\n\n  \/\/ Count items (half of children because duplicated)\n  const children = Array.from(ticker.children);\n  const halfCount = Math.max(1, Math.floor(children.length \/ 2));\n  const isMobile = window.innerWidth <= MOBILE_BREAKPOINT_PX;\n\n  \/\/ Compute duration: number of items * visible time per item\n  const visiblePerItem = isMobile ? VISIBLE_TIME_PER_ITEM_MOBILE : VISIBLE_TIME_PER_ITEM_DESKTOP;\n  let durationSec = halfCount * visiblePerItem;\n\n  \/\/ Clamp duration\n  durationSec = Math.max(MIN_DURATION_SEC, Math.min(MAX_DURATION_SEC, durationSec));\n\n  \/\/ If content physically small (single width < viewport), shorten it a bit so it loops nicely\n  const singleLoopWidth = ticker.scrollWidth \/ 2;\n  const viewportW = viewport.clientWidth || window.innerWidth;\n  if (singleLoopWidth <= viewportW * 0.9) {\n    \/\/ ensure at least 1.5s per item but avoid too long loops for tiny content\n    durationSec = Math.max(3, Math.min(12, halfCount * Math.max(1.2, visiblePerItem * 0.6)));\n  }\n\n  \/\/ Animation: translateX from 0 to -singleLoopWidth over durationSec seconds\n  const start = performance.now();\n  function step(now) {\n    if (pauseState) { rafId = requestAnimationFrame(step); return; } \/\/ paused: keep RAF alive but don't advance\n    const elapsed = (now - start) \/ 1000;\n    \/\/ loop progress: frac between 0..1\n    const t = (elapsed % durationSec) \/ durationSec;\n    const x = - Math.round(t * singleLoopWidth);\n    ticker.style.transform = `translateX(${x}px)`;\n    rafId = requestAnimationFrame(step);\n  }\n  rafId = requestAnimationFrame(step);\n}\n\n\/* ---------- Pause on hover \/ touch to allow reading ---------- *\/\nfunction attachPauseHandlers(){\n  \/\/ remove old listeners (safety)\n  ticker.removeEventListener('mouseenter', onEnter);\n  ticker.removeEventListener('mouseleave', onLeave);\n  ticker.removeEventListener('touchstart', onTouchStart);\n  ticker.removeEventListener('touchend', onTouchEnd);\n  ticker.removeEventListener('touchcancel', onTouchEnd);\n\n  function onEnter(){ pauseState = true; }\n  function onLeave(){ pauseState = false; }\n  function onTouchStart(){ pauseState = true; }\n  function onTouchEnd(){ pauseState = false; }\n\n  \/\/ desktop hover\n  ticker.addEventListener('mouseenter', onEnter);\n  ticker.addEventListener('mouseleave', onLeave);\n  \/\/ mobile touch\n  ticker.addEventListener('touchstart', onTouchStart, {passive:true});\n  ticker.addEventListener('touchend', onTouchEnd);\n  ticker.addEventListener('touchcancel', onTouchEnd);\n}\n\n\/* ---------- Recompute on resize\/orientation ---------- *\/\nlet resizeTimer = null;\nwindow.addEventListener('resize', () => {\n  clearTimeout(resizeTimer);\n  resizeTimer = setTimeout(() => {\n    \/\/ rebuild animation with new metrics\n    requestAnimationFrame(setAdaptiveAnimation);\n  }, 150);\n});\nwindow.addEventListener('orientationchange', () => {\n  setTimeout(() => requestAnimationFrame(setAdaptiveAnimation), 160);\n});\n\n\/* ---------- Init ---------- *\/\nbuildTickerContent();\nsetInterval(buildTickerContent, REFRESH_MINUTES * 60 * 1000);\n<\/script>\n\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>Planning Animcloud AC Planning Semaine de No\u00ebl Ouvrir Plein \u00c9cran Mise \u00e0 jour automatique en temps r\u00e9el. M\u00e9t\u00e9o Val Fr\u00e9jus \u2014 ticker lisible Chargement m\u00e9t\u00e9o Val Fr\u00e9jus&#8230;<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-9938","page","type-page","status-publish","hentry"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/pages\/9938","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/comments?post=9938"}],"version-history":[{"count":27,"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/pages\/9938\/revisions"}],"predecessor-version":[{"id":10024,"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/pages\/9938\/revisions\/10024"}],"wp:attachment":[{"href":"https:\/\/www.animcloud.fr\/en\/wp-json\/wp\/v2\/media?parent=9938"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}