diff --git a/.env.example b/.env.example index ad334fe..36eb0ba 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,16 @@ CORS_ORIGIN=http://localhost:5173 UPLOAD_DIR=./server/uploads DATA_DIR=./server/data +# Production (demo.cavinkare.in/citpl_website): +# 1. Copy dist/ + server/ + package.json + .env to the host +# 2. npm install --omit=dev && npm run server (or pm2 start server/index.js) +# 3. Ensure Apache serves dist/ at /citpl_website/ with public/.htaccess +# (PHP api/index.php proxies /api → 127.0.0.1:3001) +# CORS_ORIGIN=https://demo.cavinkare.in + # Leave unset to auto-use Vite base: # local: /api/... # prod: /citpl_website/api/... # Only set if the API is on a different host than the site. # VITE_API_BASE= +# SERVE_STATIC=1 diff --git a/dist/.htaccess b/dist/.htaccess index 0967df7..97dff0b 100644 --- a/dist/.htaccess +++ b/dist/.htaccess @@ -1,15 +1,30 @@ # Deployed under /citpl_website/ on Apache. -# Requires: mod_rewrite, mod_headers, and (for API) mod_proxy + mod_proxy_http +# Requires: mod_rewrite, mod_headers, PHP curl +# API: PHP proxy to Node on 127.0.0.1:3001 (start with: npm run server / pm2) -RewriteEngine On + + RewriteEngine On + RewriteBase /citpl_website/ -# Proxy Node API + uploads (start server with: node server/index.js) -RewriteRule ^api/(.*)$ http://127.0.0.1:3001/api/$1 [P,L] -RewriteRule ^uploads/(.*)$ http://127.0.0.1:3001/uploads/$1 [P,L] + # API -> PHP proxy (works without mod_proxy) + RewriteRule ^api(?:/.*)?$ api/index.php [QSA,L] -# Allow admin login/section preview to iframe this page (same origin) - - Header always unset X-Frame-Options - Header always unset Content-Security-Policy - Header always set Content-Security-Policy "frame-ancestors 'self'" - + # Uploads -> PHP proxy + RewriteRule ^uploads/(.*)$ uploads-proxy.php?file=$1 [QSA,L] + + +# Host security headers use frame-ancestors 'none' + X-Frame-Options DENY. +# Edit them so admin can embed preview.html (same origin). + + Header always edit Content-Security-Policy "frame-ancestors 'none'" "frame-ancestors 'self'" + Header always edit X-Frame-Options "DENY" "SAMEORIGIN" + + + Header unset X-Frame-Options + Header always unset X-Frame-Options + Header unset Content-Security-Policy + Header always unset Content-Security-Policy + Header always set X-Frame-Options "SAMEORIGIN" + Header always set Content-Security-Policy "frame-ancestors 'self'" + + diff --git a/dist/admin.html b/dist/admin.html index dbea2d6..1b5db50 100644 --- a/dist/admin.html +++ b/dist/admin.html @@ -4,9 +4,9 @@ CITPL Admin - + - +
diff --git a/dist/api/index.php b/dist/api/index.php new file mode 100644 index 0000000..035169d --- /dev/null +++ b/dist/api/index.php @@ -0,0 +1,119 @@ + $value) { + $lower = strtolower((string) $name); + if ($lower === 'host' || $lower === 'content-length') { + continue; + } + $headers[] = $name . ': ' . $value; + } +} else { + foreach ($_SERVER as $key => $value) { + if (strpos($key, 'HTTP_') !== 0) { + continue; + } + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + if (strtolower($name) === 'host') { + continue; + } + $headers[] = $name . ': ' . $value; + } + if (!empty($_SERVER['CONTENT_TYPE'])) { + $headers[] = 'Content-Type: ' . $_SERVER['CONTENT_TYPE']; + } +} + +if (!function_exists('curl_init')) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode([ + 'error' => 'PHP curl extension is required for the API proxy', + 'code' => 'PROXY_MISCONFIGURED', + ]); + exit; +} + +$ch = curl_init($url); +curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 60, + CURLOPT_HTTPHEADER => $headers, +]); + +if ($method !== 'GET' && $method !== 'HEAD') { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); +} + +$response = curl_exec($ch); +if ($response === false) { + $err = curl_error($ch); + curl_close($ch); + http_response_code(502); + header('Content-Type: application/json'); + echo json_encode([ + 'error' => 'API server unreachable. On the host run: npm run server (port 3001)', + 'detail' => $err, + 'code' => 'BAD_GATEWAY', + ]); + exit; +} + +$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); +$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE); +curl_close($ch); + +$rawHeaders = substr($response, 0, $headerSize); +$responseBody = substr($response, $headerSize); + +http_response_code($status > 0 ? $status : 502); + +$skip = ['transfer-encoding', 'connection', 'keep-alive', 'content-length']; +foreach (explode("\r\n", $rawHeaders) as $line) { + if ($line === '' || stripos($line, 'HTTP/') === 0) { + continue; + } + $parts = explode(':', $line, 2); + if (count($parts) < 2) { + continue; + } + $name = trim($parts[0]); + if (in_array(strtolower($name), $skip, true)) { + continue; + } + header($name . ':' . $parts[1], false); +} + +echo $responseBody; diff --git a/dist/assets/admin-Blm7TeBi.js b/dist/assets/admin-Blm7TeBi.js new file mode 100644 index 0000000..f653bad --- /dev/null +++ b/dist/assets/admin-Blm7TeBi.js @@ -0,0 +1 @@ +import{a as e,c as t,i as n,l as r,m as i,n as a,o,r as s,s as c,t as l,u}from"./x-DnG5sti3.js";var d=e(`award`,[[`path`,{d:`m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526`,key:`1yiouv`}],[`circle`,{cx:`12`,cy:`8`,r:`6`,key:`1vp47v`}]]),f=e(`building-2`,[[`path`,{d:`M10 12h4`,key:`a56b0p`}],[`path`,{d:`M10 8h4`,key:`1sr2af`}],[`path`,{d:`M14 21v-3a2 2 0 0 0-4 0v3`,key:`1rgiei`}],[`path`,{d:`M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2`,key:`secmi2`}],[`path`,{d:`M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16`,key:`16ra0t`}]]),p=e(`circle-alert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),m=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),h=e(`external-link`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),g=e(`eye`,[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),_=e(`file-text`,[[`path`,{d:`M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z`,key:`1oefj6`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10 9H8`,key:`b1mrlr`}],[`path`,{d:`M16 13H8`,key:`t4e002`}],[`path`,{d:`M16 17H8`,key:`z1uh3a`}]]),v=e(`images`,[[`path`,{d:`m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16`,key:`9kzy35`}],[`path`,{d:`M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2`,key:`1t0f0t`}],[`circle`,{cx:`13`,cy:`7`,r:`1`,fill:`currentColor`,key:`1obus6`}],[`rect`,{x:`8`,y:`2`,width:`14`,height:`14`,rx:`2`,key:`1gvhby`}]]),y=e(`layout-dashboard`,[[`rect`,{width:`7`,height:`9`,x:`3`,y:`3`,rx:`1`,key:`10lvy0`}],[`rect`,{width:`7`,height:`5`,x:`14`,y:`3`,rx:`1`,key:`16une8`}],[`rect`,{width:`7`,height:`9`,x:`14`,y:`12`,rx:`1`,key:`1hutg5`}],[`rect`,{width:`7`,height:`5`,x:`3`,y:`16`,rx:`1`,key:`ldoo1y`}]]),b=e(`layers`,[[`path`,{d:`M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z`,key:`zw3jo`}],[`path`,{d:`M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12`,key:`1wduqc`}],[`path`,{d:`M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17`,key:`kqbvx6`}]]),x=e(`link-2`,[[`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2`,key:`8i5ue5`}],[`path`,{d:`M15 7h2a5 5 0 1 1 0 10h-2`,key:`1b9ql8`}],[`line`,{x1:`8`,x2:`16`,y1:`12`,y2:`12`,key:`1jonct`}]]),S=e(`log-out`,[[`path`,{d:`m16 17 5-5-5-5`,key:`1bji2h`}],[`path`,{d:`M21 12H9`,key:`dn1m92`}],[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}]]),C=e(`package`,[[`path`,{d:`M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z`,key:`1a0edw`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}]]),w=e(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),T=e(`save`,[[`path`,{d:`M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z`,key:`1c8476`}],[`path`,{d:`M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7`,key:`1ydtos`}],[`path`,{d:`M7 3v4a1 1 0 0 0 1 1h7`,key:`t51u73`}]]),E=e(`settings`,[[`path`,{d:`M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915`,key:`1i5ecw`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),D=e(`target`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`circle`,{cx:`12`,cy:`12`,r:`6`,key:`1vlfrh`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}]]),O=e(`trash-2`,[[`path`,{d:`M10 11v6`,key:`nco0om`}],[`path`,{d:`M14 11v6`,key:`outv1u`}],[`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6`,key:`miytrc`}],[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`,key:`e791ji`}]]),k=e(`upload`,[[`path`,{d:`M12 3v12`,key:`1x0j5s`}],[`path`,{d:`m17 8-5-5-5 5`,key:`7q97r8`}],[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}]]),A=i(u(),1),j=r(),M=t();function N(){return localStorage.getItem(`admin_token`)}function P(e){localStorage.setItem(`admin_token`,e)}function F(){localStorage.removeItem(`admin_token`)}async function I(e,t={}){let n={...t.headers};t.body instanceof FormData||(n[`Content-Type`]=`application/json`);let r=N();r&&(n.Authorization=`Bearer ${r}`);let i=await fetch(`${o}${e}`,{...t,headers:n}),a=await i.json().catch(()=>({}));if(!i.ok){let e=i.status===404?`API not found. Deploy server/ and run: npm run server (port 3001)`:i.status===502?`API server unreachable. Start Node on the host (port 3001).`:`Request failed`;throw Error(a.error||e)}return a}var L={login:(e,t)=>I(`/api/auth/login`,{method:`POST`,body:JSON.stringify({username:e,password:t})}),me:()=>I(`/api/auth/me`),getContent:()=>I(`/api/content`),updateSection:(e,t)=>I(`/api/content/${e}`,{method:`PUT`,body:JSON.stringify(t)}),changePassword:(e,t)=>I(`/api/auth/password`,{method:`PUT`,body:JSON.stringify({currentPassword:e,newPassword:t})}),upload:async e=>{let t=new FormData;return t.append(`file`,e),I(`/api/upload`,{method:`POST`,body:t})}};function R({value:e,onChange:t,label:n=`Image`}){let[r,i]=(0,A.useState)(!1);return(0,M.jsxs)(`div`,{className:`form-field`,children:[(0,M.jsx)(`label`,{className:`form-label`,children:n}),e&&(0,M.jsx)(`div`,{className:`form-preview`,children:e.match(/\.(mp4|webm)$/i)?(0,M.jsx)(`video`,{src:e,controls:!0}):(0,M.jsx)(`img`,{src:e,alt:``})}),(0,M.jsx)(`input`,{type:`text`,className:`form-input`,value:e||``,onChange:e=>t(e.target.value),placeholder:`Paste URL or upload a file below`}),(0,M.jsxs)(`label`,{className:`form-upload-btn`,children:[(0,M.jsx)(k,{size:14}),r?`Uploading...`:`Choose file`,(0,M.jsx)(`input`,{type:`file`,accept:`image/*,video/mp4`,onChange:async e=>{let n=e.target.files?.[0];if(n){i(!0);try{let{url:e}=await L.upload(n);t(e)}catch(e){alert(e.message)}finally{i(!1)}}},hidden:!0,disabled:r})]})]})}function z({label:e,value:t,onChange:n,multiline:r=!1,type:i=`text`}){return(0,M.jsxs)(`div`,{className:`form-field`,children:[(0,M.jsx)(`label`,{className:`form-label`,children:e}),r?(0,M.jsx)(`textarea`,{className:`form-textarea`,value:t||``,onChange:e=>n(e.target.value),rows:4}):(0,M.jsx)(`input`,{type:i,className:`form-input`,value:t||``,onChange:e=>n(e.target.value)})]})}function B({label:e,items:t,onChange:n,fields:r}){let i=(e,r,i)=>{n(t.map((t,n)=>n===e?{...t,[r]:i}:t))},a=()=>{let e=r.reduce((e,t)=>({...e,[t.key]:t.default??``}),{id:Date.now()});n([...t,e])},o=e=>n(t.filter((t,n)=>n!==e));return(0,M.jsxs)(`div`,{className:`array-section`,children:[(0,M.jsxs)(`div`,{className:`array-header`,children:[(0,M.jsx)(`h4`,{className:`array-title`,children:e}),(0,M.jsxs)(`button`,{type:`button`,onClick:a,className:`array-add-btn`,children:[(0,M.jsx)(w,{size:14}),` Add item`]})]}),t.length===0&&(0,M.jsx)(`p`,{style:{color:`#94a3b8`,fontSize:`0.85rem`,margin:`0 0 1rem`},children:`No items yet. Click "Add item" to create one.`}),t.map((e,t)=>(0,M.jsxs)(`div`,{className:`array-item`,children:[(0,M.jsxs)(`div`,{className:`array-item-header`,children:[(0,M.jsxs)(`span`,{className:`array-item-badge`,children:[`Item `,t+1]}),(0,M.jsxs)(`button`,{type:`button`,onClick:()=>o(t),className:`array-remove-btn`,children:[(0,M.jsx)(O,{size:12}),` Remove`]})]}),r.map(n=>n.type===`image`?(0,M.jsx)(R,{label:n.label,value:e[n.key],onChange:e=>i(t,n.key,e)},n.key):(0,M.jsx)(z,{label:n.label,value:e[n.key],onChange:e=>i(t,n.key,e),multiline:n.multiline},n.key))]},e.id??t))]})}var V={hero:`home`,about:`aboutus`,certifications:`certifications`,services:`services`,whyUs:`whyus`,gallery:`featured-gallery`,products:`products`,insights:`insights`,footer:`contactus`};function H(e,t,n){if(!e||!n)return e;let r={...e};return t===`hero`?(r.hero=n.hero??r.hero,r.metrics=n.metrics??r.metrics):t===`about`?(r.about=n.about??r.about,r.partners=n.partners??r.partners):r[t]=n,r}var U=`/citpl_website/preview.html`,W=1440,G=900;function K({section:e,label:t,content:n,sectionData:r}){let i=(0,A.useRef)(null),a=(0,A.useRef)(null),o=(0,A.useRef)({}),s=(0,A.useRef)(null),c=V[e]||`home`,[l,u]=(0,A.useState)(.45),[d,f]=(0,A.useState)(!1),[p,m]=(0,A.useState)(!1),[_,v]=(0,A.useState)(!1);o.current={content:n,sectionData:r,section:e,anchor:c},(0,A.useEffect)(()=>{let e=window.setTimeout(()=>v(!0),50);return()=>window.clearTimeout(e)},[]),(0,A.useEffect)(()=>{let e=e=>{e.origin===window.location.origin&&e.data?.type===`PREVIEW_READY`&&m(!0)};return window.addEventListener(`message`,e),()=>window.removeEventListener(`message`,e)},[]),(0,A.useEffect)(()=>{let e=i.current;if(!e)return;let t=()=>{let t=e.clientWidth;u(t>0?t/W:.45)};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let y=(0,A.useCallback)((e=!1)=>{let t=()=>{let e=a.current,{content:t,sectionData:n,section:r,anchor:i}=o.current;!e?.contentWindow||!t||!n||e.contentWindow.postMessage({type:`ADMIN_PREVIEW`,section:r,anchor:i,fullContent:H(t,r,n)},window.location.origin)};if(e){window.clearTimeout(s.current),t();return}window.clearTimeout(s.current),s.current=window.setTimeout(t,200)},[]);(0,A.useEffect)(()=>{d&&y(!0)},[d,e,c,d,y]),(0,A.useEffect)(()=>{if(d)return y(!1),()=>window.clearTimeout(s.current)},[n,r,d,y]);let b=()=>{f(!0),y(!0)},x=!_||!d||!p;return(0,M.jsxs)(`aside`,{className:`admin-preview-panel`,children:[(0,M.jsxs)(`div`,{className:`admin-preview-panel-header`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsxs)(`span`,{className:`admin-preview-eyebrow`,children:[(0,M.jsx)(g,{size:14}),`Live preview`]}),(0,M.jsx)(`h3`,{children:t})]}),(0,M.jsxs)(`a`,{href:`/citpl_website/#${c}`,target:`_blank`,rel:`noopener noreferrer`,className:`admin-preview-open`,children:[(0,M.jsx)(h,{size:13}),`Open`]})]}),(0,M.jsxs)(`div`,{className:`admin-preview-shell`,children:[(0,M.jsxs)(`div`,{className:`admin-preview-chrome`,children:[(0,M.jsx)(`span`,{className:`admin-preview-dot`}),(0,M.jsx)(`span`,{className:`admin-preview-dot`}),(0,M.jsx)(`span`,{className:`admin-preview-dot`}),(0,M.jsxs)(`span`,{className:`admin-preview-url`,children:[`/`,c]})]}),(0,M.jsxs)(`div`,{className:`admin-preview-viewport`,ref:i,style:{height:G*l},children:[x&&(0,M.jsxs)(`div`,{className:`admin-preview-loading`,children:[(0,M.jsx)(`div`,{className:`admin-spinner`}),(0,M.jsx)(`span`,{children:`Loading preview…`})]}),_&&(0,M.jsx)(`div`,{className:`admin-preview-stage`,style:{width:W*l,height:G*l,opacity:+!x},children:(0,M.jsx)(`iframe`,{ref:a,src:U,title:`${t} preview`,className:`admin-preview-iframe`,onLoad:b,style:{width:W,height:G,transform:`scale(${l})`}})})]})]}),(0,M.jsx)(`p`,{className:`admin-preview-note`,children:`Updates as you edit. Save to publish to the live site.`})]})}function q(e,t){if(!e||t===`dashboard`||t===`settings`)return null;if(t===`hero`)return{hero:structuredClone(e.hero??{}),metrics:structuredClone(e.metrics??[])};if(t===`about`)return{about:structuredClone(e.about??{}),partners:structuredClone(e.partners??{logos:[]})};let n=e[t];return n?structuredClone(n):null}var J=`/citpl_website/`,Y=[{label:`Overview`,items:[{id:`dashboard`,label:`Dashboard`,icon:y,desc:`Overview and quick access to all sections`}]},{label:`Page Content`,items:[{id:`hero`,label:`Hero & Metrics`,icon:a,desc:`Homepage headline, video, and statistics`},{id:`about`,label:`About & Partners`,icon:f,desc:`About section and partner logos`},{id:`certifications`,label:`Certifications`,icon:d,desc:`Certification badges and headings`},{id:`services`,label:`Services`,icon:b,desc:`Service cards and section copy`},{id:`whyUs`,label:`Why Us`,icon:D,desc:`Value propositions and images`},{id:`gallery`,label:`Gallery`,icon:v,desc:`Featured case studies and gallery items`},{id:`products`,label:`Products`,icon:C,desc:`Product suite cards and stats`},{id:`insights`,label:`Insights`,icon:_,desc:`Blog and insight carousel items`},{id:`footer`,label:`Footer & Links`,icon:x,desc:`CTA, social links, and footer navigation`}]},{label:`Account`,items:[{id:`settings`,label:`Settings`,icon:E,desc:`Password and account preferences`}]}],X=Y.flatMap(e=>e.items),Z=Object.fromEntries(X.map(e=>[e.id,e.desc]));function Q({message:e,type:t}){return e?(0,M.jsxs)(`div`,{className:`toast ${t}`,children:[t===`success`?(0,M.jsx)(m,{size:18}):(0,M.jsx)(p,{size:18}),e]}):null}function ee(){let e=(0,A.useRef)(null),t=(0,A.useRef)(null),[n,r]=(0,A.useState)(.5),[i,a]=(0,A.useState)(!1),o=1280,s=`${J}preview.html`;return(0,A.useEffect)(()=>{let t=e.current;if(!t)return;let n=()=>{r(t.clientWidth/o)};n();let i=new ResizeObserver(n);return i.observe(t),()=>i.disconnect()},[]),(0,A.useEffect)(()=>{let e=setTimeout(()=>{try{let e=t.current?.contentDocument;(!e?.body||e.body.childElementCount===0)&&a(!0)}catch{a(!0)}},2e3);return()=>clearTimeout(e)},[]),(0,M.jsxs)(`div`,{className:`login-preview`,children:[(0,M.jsxs)(`div`,{className:`login-preview-intro`,children:[(0,M.jsx)(`img`,{src:`${J}assets/cavin_logo.svg`,alt:`Cavin Infotech`}),(0,M.jsxs)(`div`,{children:[(0,M.jsxs)(`h1`,{children:[`Content `,(0,M.jsx)(`span`,{children:`Manager`})]}),(0,M.jsx)(`p`,{children:`Preview the live website before you sign in. Edits publish instantly after login.`})]}),(0,M.jsxs)(`a`,{href:J,target:`_blank`,rel:`noopener noreferrer`,className:`login-preview-link`,children:[(0,M.jsx)(h,{size:14}),`Open full site`]})]}),(0,M.jsxs)(`div`,{className:`login-preview-shell`,children:[(0,M.jsxs)(`div`,{className:`login-preview-chrome`,children:[(0,M.jsx)(`span`,{className:`login-preview-dot`}),(0,M.jsx)(`span`,{className:`login-preview-dot`}),(0,M.jsx)(`span`,{className:`login-preview-dot`}),(0,M.jsx)(`span`,{className:`login-preview-url`,children:`cavininfotech.com`})]}),(0,M.jsx)(`div`,{className:`login-preview-viewport`,ref:e,style:{height:800*n},children:i?(0,M.jsxs)(`div`,{className:`login-preview-fallback`,children:[(0,M.jsx)(`p`,{children:`Inline preview is blocked by the server's frame policy.`}),(0,M.jsxs)(`a`,{href:s,target:`_blank`,rel:`noopener noreferrer`,className:`login-preview-link`,children:[(0,M.jsx)(h,{size:14}),`Open preview`]})]}):(0,M.jsx)(`div`,{className:`login-preview-stage`,style:{width:o*n,height:800*n},children:(0,M.jsx)(`iframe`,{ref:t,src:s,title:`Website preview`,className:`login-preview-iframe`,style:{width:o,height:800,transform:`scale(${n})`}})})})]})]})}function te({onLogin:e}){let[t,n]=(0,A.useState)(``),[r,i]=(0,A.useState)(``),[a,o]=(0,A.useState)(``),[s,c]=(0,A.useState)(!1);return(0,M.jsxs)(`div`,{className:`login-page`,children:[(0,M.jsx)(ee,{}),(0,M.jsx)(`div`,{className:`login-form-side`,children:(0,M.jsxs)(`form`,{className:`login-card`,onSubmit:async n=>{n.preventDefault(),c(!0),o(``);try{let{token:n}=await L.login(t,r);P(n),e()}catch(e){o(e.message)}finally{c(!1)}},children:[(0,M.jsx)(`h2`,{children:`Welcome back`}),(0,M.jsx)(`p`,{className:`login-subtitle`,children:`Sign in to your admin account`}),a&&(0,M.jsxs)(`div`,{className:`login-error`,children:[(0,M.jsx)(p,{size:16}),a]}),(0,M.jsx)(z,{label:`Username`,value:t,onChange:n}),(0,M.jsx)(z,{label:`Password`,value:r,onChange:i,type:`password`}),(0,M.jsx)(`button`,{type:`submit`,className:`btn-primary`,disabled:s,children:s?`Signing in...`:`Sign in`})]})})]})}function $({loading:e,error:t}){return(0,M.jsx)(`div`,{className:`admin-editor-placeholder`,children:e?(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(`div`,{className:`admin-spinner`}),(0,M.jsx)(`p`,{children:`Loading section content...`})]}):(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(p,{size:22}),(0,M.jsx)(`p`,{children:t||`Unable to load this section.`})]})})}var ne=class extends A.Component{constructor(e){super(e),this.state={error:null}}static getDerivedStateFromError(e){return{error:e}}render(){return this.state.error?(0,M.jsxs)(`div`,{className:`admin-card admin-editor-placeholder`,children:[(0,M.jsx)(p,{size:22}),(0,M.jsx)(`p`,{children:`Something went wrong loading this section.`}),(0,M.jsx)(`p`,{style:{fontSize:`0.8rem`,color:`#94a3b8`},children:this.state.error.message}),(0,M.jsx)(`button`,{type:`button`,className:`btn-secondary`,onClick:()=>this.setState({error:null}),children:`Try again`})]}):this.props.children}};function re({section:e,data:t,onChange:n}){if(!t)return(0,M.jsx)($,{loading:!1,error:`No content found for this section.`});switch(e){case`hero`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(R,{label:`Hero Video`,value:t.hero?.video,onChange:e=>n({...t,hero:{...t.hero,video:e}})}),(0,M.jsx)(z,{label:`Badge Text`,value:t.hero?.badge,onChange:e=>n({...t,hero:{...t.hero,badge:e}})}),(0,M.jsx)(z,{label:`Headline`,value:t.hero?.headline,onChange:e=>n({...t,hero:{...t.hero,headline:e}})}),(0,M.jsx)(z,{label:`Subheadline`,value:t.hero?.subheadline,onChange:e=>n({...t,hero:{...t.hero,subheadline:e}})}),(0,M.jsx)(B,{label:`Metrics`,items:t.metrics||[],onChange:e=>n({...t,metrics:e}),fields:[{key:`value`,label:`Value`},{key:`label`,label:`Label`}]})]});case`about`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`About Heading`,value:t.about?.heading,onChange:e=>n({about:{...t.about,heading:e},partners:t.partners})}),(0,M.jsx)(R,{label:`Badge Image`,value:t.about?.badgeImage,onChange:e=>n({about:{...t.about,badgeImage:e},partners:t.partners})}),(0,M.jsx)(z,{label:`Badge Alt Text`,value:t.about?.badgeAlt,onChange:e=>n({about:{...t.about,badgeAlt:e},partners:t.partners})}),(0,M.jsx)(R,{label:`Background Image`,value:t.about?.backgroundImage,onChange:e=>n({about:{...t.about,backgroundImage:e},partners:t.partners})}),(0,M.jsx)(z,{label:`Partners Heading`,value:t.partners?.heading,onChange:e=>n({about:t.about,partners:{...t.partners,heading:e}})}),(0,M.jsx)(z,{label:`Partners Subheading`,value:t.partners?.subheading,onChange:e=>n({about:t.about,partners:{...t.partners,subheading:e}})}),(0,M.jsx)(z,{label:`Partners Body`,value:t.partners?.body,onChange:e=>n({about:t.about,partners:{...t.partners,body:e}}),multiline:!0}),(0,M.jsx)(B,{label:`Partner Logos`,items:t.partners?.logos||[],onChange:e=>n({about:t.about,partners:{...t.partners,logos:e}}),fields:[{key:`name`,label:`Name`},{key:`image`,label:`Logo`,type:`image`}]})]});case`certifications`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Section Heading`,value:t.heading,onChange:e=>n({...t,heading:e})}),(0,M.jsx)(B,{label:`Certifications`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`image`,label:`Image`,type:`image`},{key:`alt`,label:`Alt Text`}]})]});case`services`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Intro`,value:t.intro,onChange:e=>n({...t,intro:e}),multiline:!0}),(0,M.jsx)(B,{label:`Service Cards`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`number`,label:`Number`},{key:`title`,label:`Title`},{key:`boldStatement`,label:`Bold Statement`},{key:`description`,label:`Description`,multiline:!0},{key:`cta`,label:`CTA Text`}]})]});case`whyUs`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(B,{label:`Value Cards`,items:t.cards||[],onChange:e=>n({...t,cards:e}),fields:[{key:`title`,label:`Title`},{key:`description`,label:`Description`,multiline:!0}]}),(0,M.jsx)(B,{label:`Images`,items:t.images||[],onChange:e=>n({...t,images:e}),fields:[{key:`src`,label:`Image`,type:`image`},{key:`alt`,label:`Alt Text`}]})]});case`gallery`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Subtitle`,value:t.subtitle,onChange:e=>n({...t,subtitle:e}),multiline:!0}),(0,M.jsx)(B,{label:`Gallery Items`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`title`,label:`Title`},{key:`category`,label:`Category`},{key:`excerpt`,label:`Excerpt`,multiline:!0},{key:`image`,label:`Image`,type:`image`},{key:`readTime`,label:`Read Time`}]})]});case`products`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Subtitle`,value:t.subtitle,onChange:e=>n({...t,subtitle:e}),multiline:!0}),(0,M.jsx)(B,{label:`Products`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`name`,label:`Name`},{key:`description`,label:`Description`,multiline:!0},{key:`image`,label:`Dashboard Image`,type:`image`}]})]});case`insights`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Subtitle`,value:t.subtitle,onChange:e=>n({...t,subtitle:e}),multiline:!0}),(0,M.jsx)(B,{label:`Insight Cards`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`category`,label:`Category`},{key:`title`,label:`Title`},{key:`date`,label:`Date`},{key:`excerpt`,label:`Excerpt`,multiline:!0},{key:`image`,label:`Image`,type:`image`}]})]});case`footer`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`CTA Headline`,value:t.cta?.headline,onChange:e=>n({...t,cta:{...t.cta,headline:e}})}),(0,M.jsx)(z,{label:`CTA Body`,value:t.cta?.body,onChange:e=>n({...t,cta:{...t.cta,body:e}}),multiline:!0}),(0,M.jsx)(R,{label:`CTA Image`,value:t.cta?.image,onChange:e=>n({...t,cta:{...t.cta,image:e}})}),(0,M.jsx)(z,{label:`Primary Button`,value:t.cta?.primaryButton,onChange:e=>n({...t,cta:{...t.cta,primaryButton:e}})}),(0,M.jsx)(z,{label:`Secondary Button`,value:t.cta?.secondaryButton,onChange:e=>n({...t,cta:{...t.cta,secondaryButton:e}})}),(0,M.jsx)(B,{label:`Quick Links`,items:t.quickLinks||[],onChange:e=>n({...t,quickLinks:e}),fields:[{key:`label`,label:`Label`},{key:`href`,label:`Link`}]}),(0,M.jsx)(B,{label:`Product Links`,items:t.productLinks||[],onChange:e=>n({...t,productLinks:e}),fields:[{key:`label`,label:`Label`},{key:`href`,label:`Link`}]}),(0,M.jsx)(B,{label:`Social Links`,items:t.social||[],onChange:e=>n({...t,social:e}),fields:[{key:`platform`,label:`Platform`},{key:`url`,label:`URL`}]}),(0,M.jsx)(B,{label:`Legal Links`,items:t.legal||[],onChange:e=>n({...t,legal:e}),fields:[{key:`label`,label:`Label`},{key:`href`,label:`Link`}]})]});default:return null}}function ie(){let[e,t]=(0,A.useState)(``),[n,r]=(0,A.useState)(``),[i,a]=(0,A.useState)(``);return(0,M.jsxs)(`div`,{className:`admin-card settings-card`,children:[(0,M.jsx)(`h1`,{children:`Settings`}),(0,M.jsx)(`p`,{className:`subtitle`,children:`Update your admin password. Use at least 6 characters.`}),(0,M.jsxs)(`form`,{onSubmit:async i=>{i.preventDefault();try{await L.changePassword(e,n),a(`Password updated successfully`),t(``),r(``)}catch(e){a(e.message)}},children:[(0,M.jsx)(z,{label:`Current Password`,value:e,onChange:t,type:`password`}),(0,M.jsx)(z,{label:`New Password`,value:n,onChange:r,type:`password`}),i&&(0,M.jsx)(`p`,{className:i.includes(`success`)?`msg-success`:`msg-error`,children:i}),(0,M.jsx)(`button`,{type:`submit`,className:`btn-primary`,style:{marginTop:`0.5rem`},children:`Update Password`})]})]})}function ae(e){return e?new Date(e).toLocaleString(`en-IN`,{day:`numeric`,month:`short`,year:`numeric`,hour:`2-digit`,minute:`2-digit`}):`—`}function oe(){let[e,t]=(0,A.useState)(!1),[r,i]=(0,A.useState)(!0),[a,o]=(0,A.useState)(`dashboard`),[u,d]=(0,A.useState)(null),[f,p]=(0,A.useState)(!1),[m,g]=(0,A.useState)(``),[_,v]=(0,A.useState)(null),[y,b]=(0,A.useState)(!1),[x,C]=(0,A.useState)({message:``,type:``}),[w,E]=(0,A.useState)(!1),[D,O]=(0,A.useState)(null),k=(e,t)=>{C({message:e,type:t}),setTimeout(()=>C({message:``,type:``}),3500)},j=e=>{if(o(e),E(!1),e===`dashboard`||e===`settings`){v(null);return}u&&v(q(u,e))};(0,A.useEffect)(()=>{let e=()=>{window.innerWidth>900&&E(!1)};return window.addEventListener(`resize`,e),()=>window.removeEventListener(`resize`,e)},[]),(0,A.useEffect)(()=>{if(!N()){i(!1);return}L.me().then(()=>{t(!0),i(!1)}).catch(()=>{F(),i(!1)})},[]),(0,A.useEffect)(()=>{if(!e)return;let t=document.createElement(`link`);t.rel=`prefetch`,t.href=`${J}preview.html`,document.head.appendChild(t),p(!0),g(``),L.getContent().then(e=>{let{_updatedAt:t,...n}=e;d(n),O(t)}).catch(e=>{console.warn(`Admin content API failed, using seed fallback:`,e.message),d(c),g(`Could not reach the API. Showing default content — start the server with npm run dev.`)}).finally(()=>p(!1))},[e]),(0,A.useEffect)(()=>{!u||a===`dashboard`||a===`settings`||v(q(u,a))},[u,a]);let P=async()=>{b(!0);try{a===`hero`?(await L.updateSection(`hero`,_.hero),await L.updateSection(`metrics`,_.metrics)):a===`about`?(await L.updateSection(`about`,_.about),await L.updateSection(`partners`,_.partners)):await L.updateSection(a,_);let{_updatedAt:e,...t}=await L.getContent();d(t),O(e),k(`Changes saved successfully`,`success`)}catch(e){k(e.message,`error`)}finally{b(!1)}};if(r)return(0,M.jsxs)(`div`,{className:`admin-loading`,children:[(0,M.jsx)(`div`,{className:`admin-spinner`}),(0,M.jsx)(`span`,{children:`Loading admin panel...`})]});if(!e)return(0,M.jsx)(te,{onLogin:()=>t(!0)});let I=X.find(e=>e.id===a),R=a!==`dashboard`&&a!==`settings`;return(0,M.jsxs)(`div`,{className:`admin-layout`,children:[(0,M.jsx)(`button`,{type:`button`,className:`admin-mobile-toggle`,"aria-label":`Toggle menu`,onClick:()=>E(e=>!e),children:w?(0,M.jsx)(l,{size:20}):(0,M.jsx)(s,{size:20})}),(0,M.jsx)(`div`,{className:`admin-sidebar-overlay ${w?`visible`:``}`,onClick:()=>E(!1)}),(0,M.jsxs)(`aside`,{className:`admin-sidebar ${w?`open`:``}`,children:[(0,M.jsxs)(`div`,{className:`admin-sidebar-brand`,children:[(0,M.jsx)(`img`,{src:`${J}assets/cavin_logo.svg`,alt:`Cavin Infotech`}),(0,M.jsx)(`h2`,{children:`Content Manager`}),(0,M.jsx)(`p`,{children:`Cavin Infotech Website`})]}),(0,M.jsx)(`nav`,{className:`admin-nav`,children:Y.map(e=>(0,M.jsxs)(`div`,{className:`admin-nav-group`,children:[(0,M.jsx)(`div`,{className:`admin-nav-group-label`,children:e.label}),e.items.map(({id:e,label:t,icon:n})=>(0,M.jsxs)(`button`,{type:`button`,className:a===e?`active`:``,onClick:()=>j(e),children:[(0,M.jsx)(n,{size:17}),t]},e))]},e.label))}),(0,M.jsx)(`div`,{className:`admin-sidebar-footer`,children:(0,M.jsxs)(`button`,{type:`button`,className:`logout-btn`,onClick:()=>{F(),t(!1)},children:[(0,M.jsx)(S,{size:15}),`Sign out`]})})]}),(0,M.jsxs)(`div`,{className:`admin-main-wrap`,children:[(0,M.jsxs)(`header`,{className:`admin-topbar`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`h1`,{className:`admin-topbar-title`,children:I?.label||`Admin`}),(0,M.jsx)(`p`,{className:`admin-topbar-sub`,children:Z[a]})]}),(0,M.jsxs)(`div`,{className:`admin-topbar-actions`,children:[(0,M.jsxs)(`a`,{href:J,target:`_blank`,rel:`noopener noreferrer`,className:`btn-ghost`,children:[(0,M.jsx)(h,{size:14}),`View site`]}),R&&(0,M.jsxs)(`button`,{type:`button`,className:`btn-primary`,onClick:P,disabled:y,children:[(0,M.jsx)(T,{size:15}),y?`Saving...`:`Save changes`]})]})]}),(0,M.jsx)(`main`,{className:`admin-main`,children:a===`dashboard`?(0,M.jsxs)(M.Fragment,{children:[(0,M.jsxs)(`div`,{className:`dashboard-welcome`,children:[(0,M.jsx)(`h2`,{children:`Welcome to your CMS`}),(0,M.jsx)(`p`,{children:`Edit any section below. All changes are saved to the database and appear on the live website immediately.`}),(0,M.jsx)(`div`,{className:`dashboard-meta`,children:(0,M.jsxs)(`span`,{className:`dashboard-meta-item`,children:[(0,M.jsx)(n,{size:13}),`Last updated: `,ae(D)]})})]}),(0,M.jsx)(`div`,{className:`dashboard-grid`,children:X.filter(e=>e.id!==`dashboard`).map(({id:e,label:t,icon:n,desc:r})=>(0,M.jsxs)(`button`,{type:`button`,className:`dashboard-tile`,onClick:()=>j(e),children:[(0,M.jsx)(`div`,{className:`dashboard-tile-icon`,children:(0,M.jsx)(n,{size:20})}),(0,M.jsx)(`h3`,{children:t}),(0,M.jsx)(`p`,{children:r})]},e))})]}):a===`settings`?(0,M.jsx)(ie,{}):(0,M.jsx)(ne,{children:(0,M.jsxs)(`div`,{className:`admin-editor-layout`,children:[(0,M.jsxs)(`div`,{className:`admin-editor-form admin-card`,children:[f||!_?(0,M.jsx)($,{loading:f,error:m}):(0,M.jsx)(re,{section:a,data:_,onChange:v},a),(0,M.jsxs)(`div`,{className:`save-bar`,children:[(0,M.jsx)(`button`,{type:`button`,className:`btn-secondary`,onClick:()=>j(`dashboard`),children:`Cancel`}),(0,M.jsxs)(`button`,{type:`button`,className:`btn-primary`,onClick:P,disabled:y||!_,children:[(0,M.jsx)(T,{size:15}),y?`Saving...`:`Save changes`]})]})]}),!f&&u&&_&&(0,M.jsx)(K,{section:a,label:I?.label||`Section`,content:u,sectionData:_})]})})})]}),(0,M.jsx)(Q,{...x})]})}(0,j.createRoot)(document.getElementById(`root`)).render((0,M.jsx)(A.StrictMode,{children:(0,M.jsx)(oe,{})})); \ No newline at end of file diff --git a/dist/assets/admin-C5-foetU.js b/dist/assets/admin-C5-foetU.js deleted file mode 100644 index acb8857..0000000 --- a/dist/assets/admin-C5-foetU.js +++ /dev/null @@ -1 +0,0 @@ -import{a as e,c as t,i as n,l as r,m as i,n as a,o,r as s,s as c,t as l,u}from"./x-DnG5sti3.js";var d=e(`award`,[[`path`,{d:`m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526`,key:`1yiouv`}],[`circle`,{cx:`12`,cy:`8`,r:`6`,key:`1vp47v`}]]),f=e(`building-2`,[[`path`,{d:`M10 12h4`,key:`a56b0p`}],[`path`,{d:`M10 8h4`,key:`1sr2af`}],[`path`,{d:`M14 21v-3a2 2 0 0 0-4 0v3`,key:`1rgiei`}],[`path`,{d:`M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2`,key:`secmi2`}],[`path`,{d:`M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16`,key:`16ra0t`}]]),p=e(`circle-alert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),m=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),h=e(`external-link`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),g=e(`eye`,[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),_=e(`file-text`,[[`path`,{d:`M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z`,key:`1oefj6`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10 9H8`,key:`b1mrlr`}],[`path`,{d:`M16 13H8`,key:`t4e002`}],[`path`,{d:`M16 17H8`,key:`z1uh3a`}]]),v=e(`images`,[[`path`,{d:`m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16`,key:`9kzy35`}],[`path`,{d:`M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2`,key:`1t0f0t`}],[`circle`,{cx:`13`,cy:`7`,r:`1`,fill:`currentColor`,key:`1obus6`}],[`rect`,{x:`8`,y:`2`,width:`14`,height:`14`,rx:`2`,key:`1gvhby`}]]),y=e(`layout-dashboard`,[[`rect`,{width:`7`,height:`9`,x:`3`,y:`3`,rx:`1`,key:`10lvy0`}],[`rect`,{width:`7`,height:`5`,x:`14`,y:`3`,rx:`1`,key:`16une8`}],[`rect`,{width:`7`,height:`9`,x:`14`,y:`12`,rx:`1`,key:`1hutg5`}],[`rect`,{width:`7`,height:`5`,x:`3`,y:`16`,rx:`1`,key:`ldoo1y`}]]),b=e(`layers`,[[`path`,{d:`M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z`,key:`zw3jo`}],[`path`,{d:`M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12`,key:`1wduqc`}],[`path`,{d:`M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17`,key:`kqbvx6`}]]),x=e(`link-2`,[[`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2`,key:`8i5ue5`}],[`path`,{d:`M15 7h2a5 5 0 1 1 0 10h-2`,key:`1b9ql8`}],[`line`,{x1:`8`,x2:`16`,y1:`12`,y2:`12`,key:`1jonct`}]]),S=e(`log-out`,[[`path`,{d:`m16 17 5-5-5-5`,key:`1bji2h`}],[`path`,{d:`M21 12H9`,key:`dn1m92`}],[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}]]),C=e(`package`,[[`path`,{d:`M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z`,key:`1a0edw`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}]]),w=e(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),T=e(`save`,[[`path`,{d:`M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z`,key:`1c8476`}],[`path`,{d:`M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7`,key:`1ydtos`}],[`path`,{d:`M7 3v4a1 1 0 0 0 1 1h7`,key:`t51u73`}]]),E=e(`settings`,[[`path`,{d:`M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915`,key:`1i5ecw`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),D=e(`target`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`circle`,{cx:`12`,cy:`12`,r:`6`,key:`1vlfrh`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}]]),O=e(`trash-2`,[[`path`,{d:`M10 11v6`,key:`nco0om`}],[`path`,{d:`M14 11v6`,key:`outv1u`}],[`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6`,key:`miytrc`}],[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`,key:`e791ji`}]]),k=e(`upload`,[[`path`,{d:`M12 3v12`,key:`1x0j5s`}],[`path`,{d:`m17 8-5-5-5 5`,key:`7q97r8`}],[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}]]),A=i(u(),1),j=r(),M=t();function N(){return localStorage.getItem(`admin_token`)}function P(e){localStorage.setItem(`admin_token`,e)}function F(){localStorage.removeItem(`admin_token`)}async function I(e,t={}){let n={...t.headers};t.body instanceof FormData||(n[`Content-Type`]=`application/json`);let r=N();r&&(n.Authorization=`Bearer ${r}`);let i=await fetch(`${o}${e}`,{...t,headers:n}),a=await i.json().catch(()=>({}));if(!i.ok)throw Error(a.error||`Request failed`);return a}var L={login:(e,t)=>I(`/api/auth/login`,{method:`POST`,body:JSON.stringify({username:e,password:t})}),me:()=>I(`/api/auth/me`),getContent:()=>I(`/api/content`),updateSection:(e,t)=>I(`/api/content/${e}`,{method:`PUT`,body:JSON.stringify(t)}),changePassword:(e,t)=>I(`/api/auth/password`,{method:`PUT`,body:JSON.stringify({currentPassword:e,newPassword:t})}),upload:async e=>{let t=new FormData;return t.append(`file`,e),I(`/api/upload`,{method:`POST`,body:t})}};function R({value:e,onChange:t,label:n=`Image`}){let[r,i]=(0,A.useState)(!1);return(0,M.jsxs)(`div`,{className:`form-field`,children:[(0,M.jsx)(`label`,{className:`form-label`,children:n}),e&&(0,M.jsx)(`div`,{className:`form-preview`,children:e.match(/\.(mp4|webm)$/i)?(0,M.jsx)(`video`,{src:e,controls:!0}):(0,M.jsx)(`img`,{src:e,alt:``})}),(0,M.jsx)(`input`,{type:`text`,className:`form-input`,value:e||``,onChange:e=>t(e.target.value),placeholder:`Paste URL or upload a file below`}),(0,M.jsxs)(`label`,{className:`form-upload-btn`,children:[(0,M.jsx)(k,{size:14}),r?`Uploading...`:`Choose file`,(0,M.jsx)(`input`,{type:`file`,accept:`image/*,video/mp4`,onChange:async e=>{let n=e.target.files?.[0];if(n){i(!0);try{let{url:e}=await L.upload(n);t(e)}catch(e){alert(e.message)}finally{i(!1)}}},hidden:!0,disabled:r})]})]})}function z({label:e,value:t,onChange:n,multiline:r=!1,type:i=`text`}){return(0,M.jsxs)(`div`,{className:`form-field`,children:[(0,M.jsx)(`label`,{className:`form-label`,children:e}),r?(0,M.jsx)(`textarea`,{className:`form-textarea`,value:t||``,onChange:e=>n(e.target.value),rows:4}):(0,M.jsx)(`input`,{type:i,className:`form-input`,value:t||``,onChange:e=>n(e.target.value)})]})}function B({label:e,items:t,onChange:n,fields:r}){let i=(e,r,i)=>{n(t.map((t,n)=>n===e?{...t,[r]:i}:t))},a=()=>{let e=r.reduce((e,t)=>({...e,[t.key]:t.default??``}),{id:Date.now()});n([...t,e])},o=e=>n(t.filter((t,n)=>n!==e));return(0,M.jsxs)(`div`,{className:`array-section`,children:[(0,M.jsxs)(`div`,{className:`array-header`,children:[(0,M.jsx)(`h4`,{className:`array-title`,children:e}),(0,M.jsxs)(`button`,{type:`button`,onClick:a,className:`array-add-btn`,children:[(0,M.jsx)(w,{size:14}),` Add item`]})]}),t.length===0&&(0,M.jsx)(`p`,{style:{color:`#94a3b8`,fontSize:`0.85rem`,margin:`0 0 1rem`},children:`No items yet. Click "Add item" to create one.`}),t.map((e,t)=>(0,M.jsxs)(`div`,{className:`array-item`,children:[(0,M.jsxs)(`div`,{className:`array-item-header`,children:[(0,M.jsxs)(`span`,{className:`array-item-badge`,children:[`Item `,t+1]}),(0,M.jsxs)(`button`,{type:`button`,onClick:()=>o(t),className:`array-remove-btn`,children:[(0,M.jsx)(O,{size:12}),` Remove`]})]}),r.map(n=>n.type===`image`?(0,M.jsx)(R,{label:n.label,value:e[n.key],onChange:e=>i(t,n.key,e)},n.key):(0,M.jsx)(z,{label:n.label,value:e[n.key],onChange:e=>i(t,n.key,e),multiline:n.multiline},n.key))]},e.id??t))]})}var V={hero:`home`,about:`aboutus`,certifications:`certifications`,services:`services`,whyUs:`whyus`,gallery:`featured-gallery`,products:`products`,insights:`insights`,footer:`contactus`};function H(e,t,n){if(!e||!n)return e;let r={...e};return t===`hero`?(r.hero=n.hero??r.hero,r.metrics=n.metrics??r.metrics):t===`about`?(r.about=n.about??r.about,r.partners=n.partners??r.partners):r[t]=n,r}var U=`/citpl_website/preview.html`,W=1440,G=900;function K({section:e,label:t,content:n,sectionData:r}){let i=(0,A.useRef)(null),a=(0,A.useRef)(null),o=(0,A.useRef)({}),s=(0,A.useRef)(null),c=V[e]||`home`,[l,u]=(0,A.useState)(.45),[d,f]=(0,A.useState)(!1),[p,m]=(0,A.useState)(!1),[_,v]=(0,A.useState)(!1);o.current={content:n,sectionData:r,section:e,anchor:c},(0,A.useEffect)(()=>{let e=window.setTimeout(()=>v(!0),50);return()=>window.clearTimeout(e)},[]),(0,A.useEffect)(()=>{let e=e=>{e.origin===window.location.origin&&e.data?.type===`PREVIEW_READY`&&m(!0)};return window.addEventListener(`message`,e),()=>window.removeEventListener(`message`,e)},[]),(0,A.useEffect)(()=>{let e=i.current;if(!e)return;let t=()=>{let t=e.clientWidth;u(t>0?t/W:.45)};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let y=(0,A.useCallback)((e=!1)=>{let t=()=>{let e=a.current,{content:t,sectionData:n,section:r,anchor:i}=o.current;!e?.contentWindow||!t||!n||e.contentWindow.postMessage({type:`ADMIN_PREVIEW`,section:r,anchor:i,fullContent:H(t,r,n)},window.location.origin)};if(e){window.clearTimeout(s.current),t();return}window.clearTimeout(s.current),s.current=window.setTimeout(t,200)},[]);(0,A.useEffect)(()=>{d&&y(!0)},[d,e,c,d,y]),(0,A.useEffect)(()=>{if(d)return y(!1),()=>window.clearTimeout(s.current)},[n,r,d,y]);let b=()=>{f(!0),y(!0)},x=!_||!d||!p;return(0,M.jsxs)(`aside`,{className:`admin-preview-panel`,children:[(0,M.jsxs)(`div`,{className:`admin-preview-panel-header`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsxs)(`span`,{className:`admin-preview-eyebrow`,children:[(0,M.jsx)(g,{size:14}),`Live preview`]}),(0,M.jsx)(`h3`,{children:t})]}),(0,M.jsxs)(`a`,{href:`/citpl_website/#${c}`,target:`_blank`,rel:`noopener noreferrer`,className:`admin-preview-open`,children:[(0,M.jsx)(h,{size:13}),`Open`]})]}),(0,M.jsxs)(`div`,{className:`admin-preview-shell`,children:[(0,M.jsxs)(`div`,{className:`admin-preview-chrome`,children:[(0,M.jsx)(`span`,{className:`admin-preview-dot`}),(0,M.jsx)(`span`,{className:`admin-preview-dot`}),(0,M.jsx)(`span`,{className:`admin-preview-dot`}),(0,M.jsxs)(`span`,{className:`admin-preview-url`,children:[`/`,c]})]}),(0,M.jsxs)(`div`,{className:`admin-preview-viewport`,ref:i,style:{height:G*l},children:[x&&(0,M.jsxs)(`div`,{className:`admin-preview-loading`,children:[(0,M.jsx)(`div`,{className:`admin-spinner`}),(0,M.jsx)(`span`,{children:`Loading preview…`})]}),_&&(0,M.jsx)(`div`,{className:`admin-preview-stage`,style:{width:W*l,height:G*l,opacity:+!x},children:(0,M.jsx)(`iframe`,{ref:a,src:U,title:`${t} preview`,className:`admin-preview-iframe`,onLoad:b,style:{width:W,height:G,transform:`scale(${l})`}})})]})]}),(0,M.jsx)(`p`,{className:`admin-preview-note`,children:`Updates as you edit. Save to publish to the live site.`})]})}function q(e,t){if(!e||t===`dashboard`||t===`settings`)return null;if(t===`hero`)return{hero:structuredClone(e.hero??{}),metrics:structuredClone(e.metrics??[])};if(t===`about`)return{about:structuredClone(e.about??{}),partners:structuredClone(e.partners??{logos:[]})};let n=e[t];return n?structuredClone(n):null}var J=`/citpl_website/`,Y=[{label:`Overview`,items:[{id:`dashboard`,label:`Dashboard`,icon:y,desc:`Overview and quick access to all sections`}]},{label:`Page Content`,items:[{id:`hero`,label:`Hero & Metrics`,icon:a,desc:`Homepage headline, video, and statistics`},{id:`about`,label:`About & Partners`,icon:f,desc:`About section and partner logos`},{id:`certifications`,label:`Certifications`,icon:d,desc:`Certification badges and headings`},{id:`services`,label:`Services`,icon:b,desc:`Service cards and section copy`},{id:`whyUs`,label:`Why Us`,icon:D,desc:`Value propositions and images`},{id:`gallery`,label:`Gallery`,icon:v,desc:`Featured case studies and gallery items`},{id:`products`,label:`Products`,icon:C,desc:`Product suite cards and stats`},{id:`insights`,label:`Insights`,icon:_,desc:`Blog and insight carousel items`},{id:`footer`,label:`Footer & Links`,icon:x,desc:`CTA, social links, and footer navigation`}]},{label:`Account`,items:[{id:`settings`,label:`Settings`,icon:E,desc:`Password and account preferences`}]}],X=Y.flatMap(e=>e.items),Z=Object.fromEntries(X.map(e=>[e.id,e.desc]));function Q({message:e,type:t}){return e?(0,M.jsxs)(`div`,{className:`toast ${t}`,children:[t===`success`?(0,M.jsx)(m,{size:18}):(0,M.jsx)(p,{size:18}),e]}):null}function ee(){let e=(0,A.useRef)(null),[t,n]=(0,A.useState)(.5),r=1280;return(0,A.useEffect)(()=>{let t=e.current;if(!t)return;let i=()=>{n(t.clientWidth/r)};i();let a=new ResizeObserver(i);return a.observe(t),()=>a.disconnect()},[]),(0,M.jsxs)(`div`,{className:`login-preview`,children:[(0,M.jsxs)(`div`,{className:`login-preview-intro`,children:[(0,M.jsx)(`img`,{src:`${J}assets/cavin_logo.svg`,alt:`Cavin Infotech`}),(0,M.jsxs)(`div`,{children:[(0,M.jsxs)(`h1`,{children:[`Content `,(0,M.jsx)(`span`,{children:`Manager`})]}),(0,M.jsx)(`p`,{children:`Preview the live website before you sign in. Edits publish instantly after login.`})]}),(0,M.jsxs)(`a`,{href:J,target:`_blank`,rel:`noopener noreferrer`,className:`login-preview-link`,children:[(0,M.jsx)(h,{size:14}),`Open full site`]})]}),(0,M.jsxs)(`div`,{className:`login-preview-shell`,children:[(0,M.jsxs)(`div`,{className:`login-preview-chrome`,children:[(0,M.jsx)(`span`,{className:`login-preview-dot`}),(0,M.jsx)(`span`,{className:`login-preview-dot`}),(0,M.jsx)(`span`,{className:`login-preview-dot`}),(0,M.jsx)(`span`,{className:`login-preview-url`,children:`cavininfotech.com`})]}),(0,M.jsx)(`div`,{className:`login-preview-viewport`,ref:e,style:{height:800*t},children:(0,M.jsx)(`div`,{className:`login-preview-stage`,style:{width:r*t,height:800*t},children:(0,M.jsx)(`iframe`,{src:`${J}preview.html`,title:`Website preview`,className:`login-preview-iframe`,style:{width:r,height:800,transform:`scale(${t})`}})})})]})]})}function te({onLogin:e}){let[t,n]=(0,A.useState)(``),[r,i]=(0,A.useState)(``),[a,o]=(0,A.useState)(``),[s,c]=(0,A.useState)(!1);return(0,M.jsxs)(`div`,{className:`login-page`,children:[(0,M.jsx)(ee,{}),(0,M.jsx)(`div`,{className:`login-form-side`,children:(0,M.jsxs)(`form`,{className:`login-card`,onSubmit:async n=>{n.preventDefault(),c(!0),o(``);try{let{token:n}=await L.login(t,r);P(n),e()}catch(e){o(e.message)}finally{c(!1)}},children:[(0,M.jsx)(`h2`,{children:`Welcome back`}),(0,M.jsx)(`p`,{className:`login-subtitle`,children:`Sign in to your admin account`}),a&&(0,M.jsxs)(`div`,{className:`login-error`,children:[(0,M.jsx)(p,{size:16}),a]}),(0,M.jsx)(z,{label:`Username`,value:t,onChange:n}),(0,M.jsx)(z,{label:`Password`,value:r,onChange:i,type:`password`}),(0,M.jsx)(`button`,{type:`submit`,className:`btn-primary`,disabled:s,children:s?`Signing in...`:`Sign in`})]})})]})}function $({loading:e,error:t}){return(0,M.jsx)(`div`,{className:`admin-editor-placeholder`,children:e?(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(`div`,{className:`admin-spinner`}),(0,M.jsx)(`p`,{children:`Loading section content...`})]}):(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(p,{size:22}),(0,M.jsx)(`p`,{children:t||`Unable to load this section.`})]})})}var ne=class extends A.Component{constructor(e){super(e),this.state={error:null}}static getDerivedStateFromError(e){return{error:e}}render(){return this.state.error?(0,M.jsxs)(`div`,{className:`admin-card admin-editor-placeholder`,children:[(0,M.jsx)(p,{size:22}),(0,M.jsx)(`p`,{children:`Something went wrong loading this section.`}),(0,M.jsx)(`p`,{style:{fontSize:`0.8rem`,color:`#94a3b8`},children:this.state.error.message}),(0,M.jsx)(`button`,{type:`button`,className:`btn-secondary`,onClick:()=>this.setState({error:null}),children:`Try again`})]}):this.props.children}};function re({section:e,data:t,onChange:n}){if(!t)return(0,M.jsx)($,{loading:!1,error:`No content found for this section.`});switch(e){case`hero`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(R,{label:`Hero Video`,value:t.hero?.video,onChange:e=>n({...t,hero:{...t.hero,video:e}})}),(0,M.jsx)(z,{label:`Badge Text`,value:t.hero?.badge,onChange:e=>n({...t,hero:{...t.hero,badge:e}})}),(0,M.jsx)(z,{label:`Headline`,value:t.hero?.headline,onChange:e=>n({...t,hero:{...t.hero,headline:e}})}),(0,M.jsx)(z,{label:`Subheadline`,value:t.hero?.subheadline,onChange:e=>n({...t,hero:{...t.hero,subheadline:e}})}),(0,M.jsx)(B,{label:`Metrics`,items:t.metrics||[],onChange:e=>n({...t,metrics:e}),fields:[{key:`value`,label:`Value`},{key:`label`,label:`Label`}]})]});case`about`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`About Heading`,value:t.about?.heading,onChange:e=>n({about:{...t.about,heading:e},partners:t.partners})}),(0,M.jsx)(R,{label:`Badge Image`,value:t.about?.badgeImage,onChange:e=>n({about:{...t.about,badgeImage:e},partners:t.partners})}),(0,M.jsx)(z,{label:`Badge Alt Text`,value:t.about?.badgeAlt,onChange:e=>n({about:{...t.about,badgeAlt:e},partners:t.partners})}),(0,M.jsx)(R,{label:`Background Image`,value:t.about?.backgroundImage,onChange:e=>n({about:{...t.about,backgroundImage:e},partners:t.partners})}),(0,M.jsx)(z,{label:`Partners Heading`,value:t.partners?.heading,onChange:e=>n({about:t.about,partners:{...t.partners,heading:e}})}),(0,M.jsx)(z,{label:`Partners Subheading`,value:t.partners?.subheading,onChange:e=>n({about:t.about,partners:{...t.partners,subheading:e}})}),(0,M.jsx)(z,{label:`Partners Body`,value:t.partners?.body,onChange:e=>n({about:t.about,partners:{...t.partners,body:e}}),multiline:!0}),(0,M.jsx)(B,{label:`Partner Logos`,items:t.partners?.logos||[],onChange:e=>n({about:t.about,partners:{...t.partners,logos:e}}),fields:[{key:`name`,label:`Name`},{key:`image`,label:`Logo`,type:`image`}]})]});case`certifications`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Section Heading`,value:t.heading,onChange:e=>n({...t,heading:e})}),(0,M.jsx)(B,{label:`Certifications`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`image`,label:`Image`,type:`image`},{key:`alt`,label:`Alt Text`}]})]});case`services`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Intro`,value:t.intro,onChange:e=>n({...t,intro:e}),multiline:!0}),(0,M.jsx)(B,{label:`Service Cards`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`number`,label:`Number`},{key:`title`,label:`Title`},{key:`boldStatement`,label:`Bold Statement`},{key:`description`,label:`Description`,multiline:!0},{key:`cta`,label:`CTA Text`}]})]});case`whyUs`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(B,{label:`Value Cards`,items:t.cards||[],onChange:e=>n({...t,cards:e}),fields:[{key:`title`,label:`Title`},{key:`description`,label:`Description`,multiline:!0}]}),(0,M.jsx)(B,{label:`Images`,items:t.images||[],onChange:e=>n({...t,images:e}),fields:[{key:`src`,label:`Image`,type:`image`},{key:`alt`,label:`Alt Text`}]})]});case`gallery`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Subtitle`,value:t.subtitle,onChange:e=>n({...t,subtitle:e}),multiline:!0}),(0,M.jsx)(B,{label:`Gallery Items`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`title`,label:`Title`},{key:`category`,label:`Category`},{key:`excerpt`,label:`Excerpt`,multiline:!0},{key:`image`,label:`Image`,type:`image`},{key:`readTime`,label:`Read Time`}]})]});case`products`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Eyebrow`,value:t.eyebrow,onChange:e=>n({...t,eyebrow:e})}),(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Subtitle`,value:t.subtitle,onChange:e=>n({...t,subtitle:e}),multiline:!0}),(0,M.jsx)(B,{label:`Products`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`name`,label:`Name`},{key:`description`,label:`Description`,multiline:!0},{key:`image`,label:`Dashboard Image`,type:`image`}]})]});case`insights`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`Title`,value:t.title,onChange:e=>n({...t,title:e})}),(0,M.jsx)(z,{label:`Subtitle`,value:t.subtitle,onChange:e=>n({...t,subtitle:e}),multiline:!0}),(0,M.jsx)(B,{label:`Insight Cards`,items:t.items||[],onChange:e=>n({...t,items:e}),fields:[{key:`category`,label:`Category`},{key:`title`,label:`Title`},{key:`date`,label:`Date`},{key:`excerpt`,label:`Excerpt`,multiline:!0},{key:`image`,label:`Image`,type:`image`}]})]});case`footer`:return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(z,{label:`CTA Headline`,value:t.cta?.headline,onChange:e=>n({...t,cta:{...t.cta,headline:e}})}),(0,M.jsx)(z,{label:`CTA Body`,value:t.cta?.body,onChange:e=>n({...t,cta:{...t.cta,body:e}}),multiline:!0}),(0,M.jsx)(R,{label:`CTA Image`,value:t.cta?.image,onChange:e=>n({...t,cta:{...t.cta,image:e}})}),(0,M.jsx)(z,{label:`Primary Button`,value:t.cta?.primaryButton,onChange:e=>n({...t,cta:{...t.cta,primaryButton:e}})}),(0,M.jsx)(z,{label:`Secondary Button`,value:t.cta?.secondaryButton,onChange:e=>n({...t,cta:{...t.cta,secondaryButton:e}})}),(0,M.jsx)(B,{label:`Quick Links`,items:t.quickLinks||[],onChange:e=>n({...t,quickLinks:e}),fields:[{key:`label`,label:`Label`},{key:`href`,label:`Link`}]}),(0,M.jsx)(B,{label:`Product Links`,items:t.productLinks||[],onChange:e=>n({...t,productLinks:e}),fields:[{key:`label`,label:`Label`},{key:`href`,label:`Link`}]}),(0,M.jsx)(B,{label:`Social Links`,items:t.social||[],onChange:e=>n({...t,social:e}),fields:[{key:`platform`,label:`Platform`},{key:`url`,label:`URL`}]}),(0,M.jsx)(B,{label:`Legal Links`,items:t.legal||[],onChange:e=>n({...t,legal:e}),fields:[{key:`label`,label:`Label`},{key:`href`,label:`Link`}]})]});default:return null}}function ie(){let[e,t]=(0,A.useState)(``),[n,r]=(0,A.useState)(``),[i,a]=(0,A.useState)(``);return(0,M.jsxs)(`div`,{className:`admin-card settings-card`,children:[(0,M.jsx)(`h1`,{children:`Settings`}),(0,M.jsx)(`p`,{className:`subtitle`,children:`Update your admin password. Use at least 6 characters.`}),(0,M.jsxs)(`form`,{onSubmit:async i=>{i.preventDefault();try{await L.changePassword(e,n),a(`Password updated successfully`),t(``),r(``)}catch(e){a(e.message)}},children:[(0,M.jsx)(z,{label:`Current Password`,value:e,onChange:t,type:`password`}),(0,M.jsx)(z,{label:`New Password`,value:n,onChange:r,type:`password`}),i&&(0,M.jsx)(`p`,{className:i.includes(`success`)?`msg-success`:`msg-error`,children:i}),(0,M.jsx)(`button`,{type:`submit`,className:`btn-primary`,style:{marginTop:`0.5rem`},children:`Update Password`})]})]})}function ae(e){return e?new Date(e).toLocaleString(`en-IN`,{day:`numeric`,month:`short`,year:`numeric`,hour:`2-digit`,minute:`2-digit`}):`—`}function oe(){let[e,t]=(0,A.useState)(!1),[r,i]=(0,A.useState)(!0),[a,o]=(0,A.useState)(`dashboard`),[u,d]=(0,A.useState)(null),[f,p]=(0,A.useState)(!1),[m,g]=(0,A.useState)(``),[_,v]=(0,A.useState)(null),[y,b]=(0,A.useState)(!1),[x,C]=(0,A.useState)({message:``,type:``}),[w,E]=(0,A.useState)(!1),[D,O]=(0,A.useState)(null),k=(e,t)=>{C({message:e,type:t}),setTimeout(()=>C({message:``,type:``}),3500)},j=e=>{if(o(e),E(!1),e===`dashboard`||e===`settings`){v(null);return}u&&v(q(u,e))};(0,A.useEffect)(()=>{let e=()=>{window.innerWidth>900&&E(!1)};return window.addEventListener(`resize`,e),()=>window.removeEventListener(`resize`,e)},[]),(0,A.useEffect)(()=>{if(!N()){i(!1);return}L.me().then(()=>{t(!0),i(!1)}).catch(()=>{F(),i(!1)})},[]),(0,A.useEffect)(()=>{if(!e)return;let t=document.createElement(`link`);t.rel=`prefetch`,t.href=`${J}preview.html`,document.head.appendChild(t),p(!0),g(``),L.getContent().then(e=>{let{_updatedAt:t,...n}=e;d(n),O(t)}).catch(e=>{console.warn(`Admin content API failed, using seed fallback:`,e.message),d(c),g(`Could not reach the API. Showing default content — start the server with npm run dev.`)}).finally(()=>p(!1))},[e]),(0,A.useEffect)(()=>{!u||a===`dashboard`||a===`settings`||v(q(u,a))},[u,a]);let P=async()=>{b(!0);try{a===`hero`?(await L.updateSection(`hero`,_.hero),await L.updateSection(`metrics`,_.metrics)):a===`about`?(await L.updateSection(`about`,_.about),await L.updateSection(`partners`,_.partners)):await L.updateSection(a,_);let{_updatedAt:e,...t}=await L.getContent();d(t),O(e),k(`Changes saved successfully`,`success`)}catch(e){k(e.message,`error`)}finally{b(!1)}};if(r)return(0,M.jsxs)(`div`,{className:`admin-loading`,children:[(0,M.jsx)(`div`,{className:`admin-spinner`}),(0,M.jsx)(`span`,{children:`Loading admin panel...`})]});if(!e)return(0,M.jsx)(te,{onLogin:()=>t(!0)});let I=X.find(e=>e.id===a),R=a!==`dashboard`&&a!==`settings`;return(0,M.jsxs)(`div`,{className:`admin-layout`,children:[(0,M.jsx)(`button`,{type:`button`,className:`admin-mobile-toggle`,"aria-label":`Toggle menu`,onClick:()=>E(e=>!e),children:w?(0,M.jsx)(l,{size:20}):(0,M.jsx)(s,{size:20})}),(0,M.jsx)(`div`,{className:`admin-sidebar-overlay ${w?`visible`:``}`,onClick:()=>E(!1)}),(0,M.jsxs)(`aside`,{className:`admin-sidebar ${w?`open`:``}`,children:[(0,M.jsxs)(`div`,{className:`admin-sidebar-brand`,children:[(0,M.jsx)(`img`,{src:`${J}assets/cavin_logo.svg`,alt:`Cavin Infotech`}),(0,M.jsx)(`h2`,{children:`Content Manager`}),(0,M.jsx)(`p`,{children:`Cavin Infotech Website`})]}),(0,M.jsx)(`nav`,{className:`admin-nav`,children:Y.map(e=>(0,M.jsxs)(`div`,{className:`admin-nav-group`,children:[(0,M.jsx)(`div`,{className:`admin-nav-group-label`,children:e.label}),e.items.map(({id:e,label:t,icon:n})=>(0,M.jsxs)(`button`,{type:`button`,className:a===e?`active`:``,onClick:()=>j(e),children:[(0,M.jsx)(n,{size:17}),t]},e))]},e.label))}),(0,M.jsx)(`div`,{className:`admin-sidebar-footer`,children:(0,M.jsxs)(`button`,{type:`button`,className:`logout-btn`,onClick:()=>{F(),t(!1)},children:[(0,M.jsx)(S,{size:15}),`Sign out`]})})]}),(0,M.jsxs)(`div`,{className:`admin-main-wrap`,children:[(0,M.jsxs)(`header`,{className:`admin-topbar`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`h1`,{className:`admin-topbar-title`,children:I?.label||`Admin`}),(0,M.jsx)(`p`,{className:`admin-topbar-sub`,children:Z[a]})]}),(0,M.jsxs)(`div`,{className:`admin-topbar-actions`,children:[(0,M.jsxs)(`a`,{href:J,target:`_blank`,rel:`noopener noreferrer`,className:`btn-ghost`,children:[(0,M.jsx)(h,{size:14}),`View site`]}),R&&(0,M.jsxs)(`button`,{type:`button`,className:`btn-primary`,onClick:P,disabled:y,children:[(0,M.jsx)(T,{size:15}),y?`Saving...`:`Save changes`]})]})]}),(0,M.jsx)(`main`,{className:`admin-main`,children:a===`dashboard`?(0,M.jsxs)(M.Fragment,{children:[(0,M.jsxs)(`div`,{className:`dashboard-welcome`,children:[(0,M.jsx)(`h2`,{children:`Welcome to your CMS`}),(0,M.jsx)(`p`,{children:`Edit any section below. All changes are saved to the database and appear on the live website immediately.`}),(0,M.jsx)(`div`,{className:`dashboard-meta`,children:(0,M.jsxs)(`span`,{className:`dashboard-meta-item`,children:[(0,M.jsx)(n,{size:13}),`Last updated: `,ae(D)]})})]}),(0,M.jsx)(`div`,{className:`dashboard-grid`,children:X.filter(e=>e.id!==`dashboard`).map(({id:e,label:t,icon:n,desc:r})=>(0,M.jsxs)(`button`,{type:`button`,className:`dashboard-tile`,onClick:()=>j(e),children:[(0,M.jsx)(`div`,{className:`dashboard-tile-icon`,children:(0,M.jsx)(n,{size:20})}),(0,M.jsx)(`h3`,{children:t}),(0,M.jsx)(`p`,{children:r})]},e))})]}):a===`settings`?(0,M.jsx)(ie,{}):(0,M.jsx)(ne,{children:(0,M.jsxs)(`div`,{className:`admin-editor-layout`,children:[(0,M.jsxs)(`div`,{className:`admin-editor-form admin-card`,children:[f||!_?(0,M.jsx)($,{loading:f,error:m}):(0,M.jsx)(re,{section:a,data:_,onChange:v},a),(0,M.jsxs)(`div`,{className:`save-bar`,children:[(0,M.jsx)(`button`,{type:`button`,className:`btn-secondary`,onClick:()=>j(`dashboard`),children:`Cancel`}),(0,M.jsxs)(`button`,{type:`button`,className:`btn-primary`,onClick:P,disabled:y||!_,children:[(0,M.jsx)(T,{size:15}),y?`Saving...`:`Save changes`]})]})]}),!f&&u&&_&&(0,M.jsx)(K,{section:a,label:I?.label||`Section`,content:u,sectionData:_})]})})})]}),(0,M.jsx)(Q,{...x})]})}(0,j.createRoot)(document.getElementById(`root`)).render((0,M.jsx)(A.StrictMode,{children:(0,M.jsx)(oe,{})})); \ No newline at end of file diff --git a/dist/assets/admin-Chd5uVZT.css b/dist/assets/admin-yhMCgkf-.css similarity index 80% rename from dist/assets/admin-Chd5uVZT.css rename to dist/assets/admin-yhMCgkf-.css index e13e55d..16bdae7 100644 --- a/dist/assets/admin-Chd5uVZT.css +++ b/dist/assets/admin-yhMCgkf-.css @@ -1 +1 @@ -@import "https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..900&display=swap";:root{--admin-bg:#f0f2f7;--admin-surface:#fff;--admin-sidebar:#0a1628;--admin-sidebar-hover:#ffffff0f;--admin-sidebar-active:#ffaa001f;--admin-gold:#fa0;--admin-gold-dim:#ffaa0059;--admin-text:#0f172a;--admin-muted:#64748b;--admin-border:#e2e8f0;--admin-primary:#1e40af;--admin-primary-hover:#1d4ed8;--admin-radius:12px;--admin-radius-lg:16px;--admin-shadow:0 1px 3px #0f172a0f, 0 4px 16px #0f172a0a;--admin-shadow-lg:0 8px 32px #0f172a1a;--sidebar-width:260px}*{box-sizing:border-box}body{background:var(--admin-bg);color:var(--admin-text);-webkit-font-smoothing:antialiased;margin:0;font-family:Schibsted Grotesk,system-ui,sans-serif}.admin-layout{min-height:100vh;display:flex}.admin-sidebar{width:var(--sidebar-width);background:var(--admin-sidebar);color:#e2e8f0;z-index:60;pointer-events:auto;border-right:1px solid #ffffff0f;flex-direction:column;flex-shrink:0;display:flex;position:fixed;top:0;bottom:0;left:0}.admin-sidebar-brand{border-bottom:1px solid #ffffff0f;padding:1.5rem 1.25rem 1.25rem}.admin-sidebar-brand img{height:36px;margin-bottom:.75rem;display:block}.admin-sidebar-brand h2{letter-spacing:.14em;text-transform:uppercase;color:var(--admin-gold);margin:0;font-size:.7rem;font-weight:700}.admin-sidebar-brand p{color:#64748b;margin:.35rem 0 0;font-size:.75rem}.admin-nav{flex:1;padding:.75rem .65rem;overflow-y:auto}.admin-nav-group{margin-bottom:1.25rem}.admin-nav-group-label{letter-spacing:.1em;text-transform:uppercase;color:#475569;margin-bottom:.35rem;padding:0 .75rem;font-size:.65rem;font-weight:700}.admin-nav button{text-align:left;color:#94a3b8;cursor:pointer;background:0 0;border:none;border-radius:8px;align-items:center;gap:.65rem;width:100%;padding:.6rem .75rem;font-family:inherit;font-size:.875rem;font-weight:500;transition:all .15s;display:flex}.admin-nav button svg{opacity:.7;flex-shrink:0}.admin-nav button:hover{background:var(--admin-sidebar-hover);color:#f1f5f9}.admin-nav button.active{background:var(--admin-sidebar-active);color:#fff;box-shadow:inset 3px 0 0 var(--admin-gold)}.admin-nav button.active svg{color:var(--admin-gold);opacity:1}.admin-sidebar-footer{border-top:1px solid #ffffff0f;padding:1rem .65rem 1.25rem}.logout-btn{color:#94a3b8;cursor:pointer;background:#ffffff0a;border:1px solid #ffffff1a;border-radius:8px;justify-content:center;align-items:center;gap:.5rem;width:100%;padding:.6rem 1rem;font-family:inherit;font-size:.8rem;font-weight:600;transition:all .15s;display:flex}.logout-btn:hover{color:#fca5a5;background:#ef44441f;border-color:#ef44444d}.admin-main-wrap{margin-left:var(--sidebar-width);min-width:0;width:calc(100% - var(--sidebar-width));z-index:1;flex-direction:column;flex:1;display:flex;position:relative}.admin-topbar{z-index:30;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--admin-border);background:#f0f2f7d9;justify-content:space-between;align-items:center;gap:1rem;padding:.85rem 2rem;display:flex;position:sticky;top:0}.admin-topbar-title{color:var(--admin-text);margin:0;font-size:1.15rem;font-weight:700}.admin-topbar-sub{color:var(--admin-muted);margin:.15rem 0 0;font-size:.8rem}.admin-topbar-actions{align-items:center;gap:.65rem;display:flex}.admin-main{flex:1;width:100%;padding:1.75rem 2rem 2.5rem}.admin-card{background:var(--admin-surface);border-radius:var(--admin-radius-lg);box-shadow:var(--admin-shadow);border:1px solid var(--admin-border);padding:2rem}.admin-card h1{color:var(--admin-text);letter-spacing:-.02em;margin:0 0 .35rem;font-size:1.625rem;font-weight:800}.admin-card .subtitle{color:var(--admin-muted);margin:0 0 1.75rem;font-size:.9rem;line-height:1.5}.admin-page-header{margin-bottom:1.5rem}.dashboard-welcome{border-radius:var(--admin-radius-lg);color:#fff;background:linear-gradient(135deg,#0a1628 0%,#1e3a5f 100%);border:1px solid #ffaa0026;margin-bottom:1.75rem;padding:1.75rem 2rem}.dashboard-welcome h2{margin:0 0 .35rem;font-size:1.35rem;font-weight:700}.dashboard-welcome p{color:#94a3b8;margin:0;font-size:.875rem;line-height:1.5}.dashboard-meta{flex-wrap:wrap;gap:1rem;margin-top:1.25rem;display:flex}.dashboard-meta-item{color:#cbd5e1;background:#ffffff0f;border-radius:999px;align-items:center;gap:.4rem;padding:.35rem .75rem;font-size:.75rem;display:flex}.dashboard-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;display:grid}.dashboard-tile{background:var(--admin-surface);border:1px solid var(--admin-border);border-radius:var(--admin-radius);cursor:pointer;text-align:left;box-shadow:var(--admin-shadow);padding:1.25rem;font-family:inherit;transition:all .2s}.dashboard-tile:hover{border-color:var(--admin-gold-dim);box-shadow:var(--admin-shadow-lg);transform:translateY(-2px)}.dashboard-tile-icon{width:40px;height:40px;color:var(--admin-primary);background:linear-gradient(135deg,#1e40af1a 0%,#ffaa0014 100%);border-radius:10px;justify-content:center;align-items:center;margin-bottom:.85rem;display:flex}.dashboard-tile h3{color:var(--admin-text);margin:0 0 .25rem;font-size:.95rem;font-weight:700}.dashboard-tile p{color:var(--admin-muted);margin:0;font-size:.78rem;line-height:1.4}.btn-primary{color:#fff;cursor:pointer;background:linear-gradient(135deg,#1e40af 0%,#1d4ed8 100%);border:none;border-radius:8px;justify-content:center;align-items:center;gap:.4rem;padding:.65rem 1.35rem;font-family:inherit;font-size:.875rem;font-weight:600;transition:all .15s;display:inline-flex;box-shadow:0 2px 8px #1e40af40}.btn-primary:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px #1e40af59}.btn-primary:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn-secondary{background:var(--admin-surface);color:#475569;border:1px solid var(--admin-border);cursor:pointer;border-radius:8px;justify-content:center;align-items:center;gap:.4rem;padding:.65rem 1.25rem;font-family:inherit;font-size:.875rem;font-weight:600;transition:all .15s;display:inline-flex}.btn-secondary:hover{background:#f8fafc;border-color:#cbd5e1}.btn-ghost{color:var(--admin-muted);border:1px solid var(--admin-border);cursor:pointer;background:0 0;border-radius:8px;align-items:center;gap:.4rem;padding:.5rem .85rem;font-family:inherit;font-size:.8rem;font-weight:600;text-decoration:none;transition:all .15s;display:inline-flex}.btn-ghost:hover{color:var(--admin-primary);border-color:var(--admin-primary);background:#eff6ff}.save-bar{border-top:1px solid var(--admin-border);justify-content:flex-end;gap:.75rem;margin-top:2rem;padding-top:1.5rem;display:flex}.admin-editor-layout{grid-template-columns:minmax(0,1fr) minmax(340px,42%);align-items:start;gap:1.25rem;display:grid}.admin-editor-form{min-width:0}.admin-editor-placeholder{text-align:center;min-height:240px;color:var(--admin-muted);flex-direction:column;justify-content:center;align-items:center;gap:.85rem;display:flex}.admin-editor-placeholder p{max-width:360px;margin:0;font-size:.9rem;line-height:1.5}.admin-editor-placeholder svg{color:#dc2626}.admin-preview-panel{min-width:0;position:sticky;top:5.25rem}.admin-preview-panel-header{justify-content:space-between;align-items:flex-start;gap:.75rem;margin-bottom:.75rem;display:flex}.admin-preview-eyebrow{letter-spacing:.08em;text-transform:uppercase;color:var(--admin-primary);align-items:center;gap:.35rem;font-size:.72rem;font-weight:700;display:inline-flex}.admin-preview-panel-header h3{color:var(--admin-text);margin:.25rem 0 0;font-size:1rem;font-weight:700}.admin-preview-open{color:var(--admin-muted);border:1px solid var(--admin-border);background:var(--admin-surface);white-space:nowrap;border-radius:8px;align-items:center;gap:.35rem;padding:.4rem .7rem;font-size:.78rem;font-weight:600;text-decoration:none;display:inline-flex}.admin-preview-open:hover{color:var(--admin-primary);border-color:#1e40af40}.admin-preview-panel .admin-preview-shell{border:1px solid var(--admin-border);box-shadow:var(--admin-shadow-lg);background:#0f172a;border-radius:14px;overflow:hidden}.admin-preview-panel .admin-preview-chrome{background:#0f172a;border-bottom:1px solid #ffffff14;align-items:center;gap:.45rem;padding:.65rem .85rem;display:flex}.admin-preview-panel .admin-preview-dot{border-radius:50%;width:9px;height:9px}.admin-preview-panel .admin-preview-dot:first-child{background:#f87171}.admin-preview-panel .admin-preview-dot:nth-child(2){background:#fbbf24}.admin-preview-panel .admin-preview-dot:nth-child(3){background:#4ade80}.admin-preview-panel .admin-preview-url{color:#94a3b8;background:#020710a6;border:1px solid #ffffff0f;border-radius:999px;margin-left:.45rem;padding:.28rem .7rem;font-size:.7rem}.admin-preview-panel .admin-preview-viewport{isolation:isolate;background:#020710;width:100%;position:relative;overflow:hidden}.admin-preview-stage,.login-preview-stage{max-width:100%;position:relative;overflow:hidden}.admin-preview-panel .admin-preview-iframe{transform-origin:0 0;pointer-events:auto;background:#fff;border:0;position:absolute;top:0;left:0}.admin-preview-loading{color:#94a3b8;z-index:2;background:#020710;flex-direction:column;justify-content:center;align-items:center;gap:.75rem;font-size:.85rem;display:flex;position:absolute;inset:0}.admin-preview-note{color:var(--admin-muted);margin:.65rem 0 0;font-size:.75rem;line-height:1.4}.form-field{margin-bottom:1.25rem}.form-label{color:#334155;letter-spacing:.02em;margin-bottom:.4rem;font-size:.78rem;font-weight:700;display:block}.form-input,.form-textarea{border:1px solid var(--admin-border);width:100%;color:var(--admin-text);background:#fafbfc;border-radius:8px;padding:.7rem .85rem;font-family:inherit;font-size:.9rem;transition:border-color .15s,box-shadow .15s,background .15s}.form-input:focus,.form-textarea:focus{border-color:var(--admin-primary);background:#fff;outline:none;box-shadow:0 0 0 3px #1e40af1f}.form-textarea{resize:vertical;min-height:96px;line-height:1.5}.form-preview{border:1px solid var(--admin-border);background:#f1f5f9;border-radius:10px;max-height:140px;margin-bottom:.65rem;overflow:hidden}.form-preview img,.form-preview video{object-fit:cover;width:100%;max-height:140px;display:block}.form-upload-btn{color:#475569;cursor:pointer;background:#f1f5f9;border:1px dashed #cbd5e1;border-radius:8px;align-items:center;gap:.4rem;margin-top:.5rem;padding:.45rem 1rem;font-size:.8rem;font-weight:600;transition:all .15s;display:inline-flex}.form-upload-btn:hover{border-color:var(--admin-primary);color:var(--admin-primary);background:#eff6ff}.array-section{border-top:1px solid var(--admin-border);margin-top:2rem;padding-top:1.5rem}.array-header{justify-content:space-between;align-items:center;margin-bottom:1rem;display:flex}.array-title{color:var(--admin-text);margin:0;font-size:1rem;font-weight:700}.array-add-btn{background:var(--admin-primary);color:#fff;cursor:pointer;border:none;border-radius:8px;align-items:center;gap:.35rem;padding:.4rem .85rem;font-family:inherit;font-size:.8rem;font-weight:600;transition:background .15s;display:inline-flex}.array-add-btn:hover{background:var(--admin-primary-hover)}.array-item{border-radius:var(--admin-radius);border:1px solid var(--admin-border);background:#f8fafc;margin-bottom:1rem;padding:1.25rem}.array-item-header{border-bottom:1px solid var(--admin-border);justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;display:flex}.array-item-badge{color:var(--admin-primary);background:#eff6ff;border-radius:999px;padding:.2rem .6rem;font-size:.75rem;font-weight:700}.array-remove-btn{color:#dc2626;cursor:pointer;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;align-items:center;gap:.3rem;padding:.3rem .65rem;font-family:inherit;font-size:.75rem;font-weight:600;transition:all .15s;display:inline-flex}.array-remove-btn:hover{background:#fee2e2}.login-page{grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);min-height:100vh;display:grid}.login-preview{background:linear-gradient(160deg,#020710 0%,#0a1628 40%,#1e3a5f 100%);flex-direction:column;gap:1.25rem;min-height:100vh;padding:2rem 2rem 2.25rem;display:flex;position:relative;overflow:hidden}.login-preview:before{content:"";pointer-events:none;background:radial-gradient(circle,#ffaa001f 0%,#0000 70%);width:400px;height:400px;position:absolute;top:-20%;right:-10%}.login-preview-intro{z-index:1;align-items:flex-start;gap:1rem;display:flex;position:relative}.login-preview-intro img{flex-shrink:0;height:40px}.login-preview-intro h1{color:#fff;letter-spacing:-.03em;margin:0 0 .35rem;font-size:1.5rem;font-weight:800}.login-preview-intro h1 span{color:var(--admin-gold)}.login-preview-intro p{color:#94a3b8;max-width:420px;margin:0;font-size:.85rem;line-height:1.5}.login-preview-link{color:#cbd5e1;white-space:nowrap;background:#ffffff0a;border:1px solid #ffffff1f;border-radius:999px;align-items:center;gap:.4rem;margin-left:auto;padding:.5rem .85rem;font-size:.8rem;font-weight:600;text-decoration:none;transition:all .15s;display:inline-flex}.login-preview-link:hover{color:#fff;background:#ffaa0014;border-color:#ffaa0059}.login-preview-shell{z-index:1;background:#0f172a;border:1px solid #ffffff1a;border-radius:14px;flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden;box-shadow:0 24px 64px #02071073}.login-preview-chrome{background:#0f172af2;border-bottom:1px solid #ffffff14;align-items:center;gap:.45rem;padding:.7rem .9rem;display:flex}.login-preview-dot{background:#94a3b859;border-radius:50%;width:10px;height:10px}.login-preview-dot:first-child{background:#f87171}.login-preview-dot:nth-child(2){background:#fbbf24}.login-preview-dot:nth-child(3){background:#4ade80}.login-preview-url{color:#94a3b8;background:#020710a6;border:1px solid #ffffff0f;border-radius:999px;margin-left:.5rem;padding:.3rem .75rem;font-size:.72rem}.login-preview-viewport{isolation:isolate;background:#020710;width:100%;position:relative;overflow:hidden}.login-preview-iframe{transform-origin:0 0;pointer-events:auto;background:#fff;border:0;position:absolute;top:0;left:0}.login-form-side{background:var(--admin-bg);justify-content:center;align-items:center;padding:2rem;display:flex}.login-card{background:var(--admin-surface);border-radius:var(--admin-radius-lg);width:100%;max-width:400px;box-shadow:var(--admin-shadow-lg);border:1px solid var(--admin-border);padding:2.5rem}.login-card h2{color:var(--admin-text);margin:0 0 .25rem;font-size:1.35rem;font-weight:800}.login-card .login-subtitle{color:var(--admin-muted);margin:0 0 1.75rem;font-size:.875rem}.login-card .form-field{margin-bottom:1rem}.login-card .btn-primary{width:100%;margin-top:.5rem;padding:.8rem;font-size:.95rem}.login-error{color:#dc2626;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;align-items:center;gap:.5rem;margin-bottom:1rem;padding:.65rem .85rem;font-size:.8rem;display:flex}.toast{z-index:100;box-shadow:var(--admin-shadow-lg);border-radius:10px;align-items:center;gap:.5rem;padding:.85rem 1.25rem;font-size:.875rem;font-weight:600;animation:.3s slideIn;display:flex;position:fixed;bottom:1.5rem;right:1.5rem}.toast.success{color:#166534;background:#fff;border:1px solid #bbf7d0}.toast.error{color:#dc2626;background:#fff;border:1px solid #fecaca}@keyframes slideIn{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}.admin-loading{background:var(--admin-bg);min-height:100vh;color:var(--admin-muted);flex-direction:column;justify-content:center;align-items:center;gap:1rem;display:flex}.admin-spinner{border:3px solid var(--admin-border);border-top-color:var(--admin-primary);border-radius:50%;width:36px;height:36px;animation:.7s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.settings-card{max-width:480px}.msg-success{color:#166534;margin:.5rem 0;font-size:.875rem}.msg-error{color:#dc2626;margin:.5rem 0;font-size:.875rem}.admin-mobile-toggle{z-index:70;background:var(--admin-sidebar);color:#fff;cursor:pointer;border:1px solid #ffffff1a;border-radius:8px;justify-content:center;align-items:center;width:42px;height:42px;display:none;position:fixed;top:.85rem;left:.85rem}.admin-sidebar-overlay{z-index:55;background:#02071099;display:none;position:fixed;inset:0}@media (width>=901px){.admin-sidebar-overlay{display:none!important}.admin-sidebar{transform:none!important}}@media (width<=900px){.login-page{grid-template-columns:1fr}.login-preview{gap:.85rem;min-height:auto;padding:1.25rem 1rem 1rem}.login-preview-intro{flex-wrap:wrap}.login-preview-intro h1{font-size:1.25rem}.login-preview-link{margin-left:0}.login-preview-shell{min-height:220px}.admin-mobile-toggle{display:flex}.admin-sidebar{transition:transform .25s;transform:translate(-100%)}.admin-sidebar.open{transform:translate(0)}.admin-sidebar-overlay.visible{display:block}.admin-main-wrap{width:100%;margin-left:0}.admin-topbar{padding:.85rem 1rem .85rem 3.5rem}.admin-main{padding:1.25rem 1rem 2rem}.dashboard-grid,.admin-editor-layout{grid-template-columns:1fr}.admin-preview-panel{order:-1;position:static}} +@import "https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..900&display=swap";:root{--admin-bg:#f0f2f7;--admin-surface:#fff;--admin-sidebar:#0a1628;--admin-sidebar-hover:#ffffff0f;--admin-sidebar-active:#ffaa001f;--admin-gold:#fa0;--admin-gold-dim:#ffaa0059;--admin-text:#0f172a;--admin-muted:#64748b;--admin-border:#e2e8f0;--admin-primary:#1e40af;--admin-primary-hover:#1d4ed8;--admin-radius:12px;--admin-radius-lg:16px;--admin-shadow:0 1px 3px #0f172a0f, 0 4px 16px #0f172a0a;--admin-shadow-lg:0 8px 32px #0f172a1a;--sidebar-width:260px}*{box-sizing:border-box}body{background:var(--admin-bg);color:var(--admin-text);-webkit-font-smoothing:antialiased;margin:0;font-family:Schibsted Grotesk,system-ui,sans-serif}.admin-layout{min-height:100vh;display:flex}.admin-sidebar{width:var(--sidebar-width);background:var(--admin-sidebar);color:#e2e8f0;z-index:60;pointer-events:auto;border-right:1px solid #ffffff0f;flex-direction:column;flex-shrink:0;display:flex;position:fixed;top:0;bottom:0;left:0}.admin-sidebar-brand{border-bottom:1px solid #ffffff0f;padding:1.5rem 1.25rem 1.25rem}.admin-sidebar-brand img{height:36px;margin-bottom:.75rem;display:block}.admin-sidebar-brand h2{letter-spacing:.14em;text-transform:uppercase;color:var(--admin-gold);margin:0;font-size:.7rem;font-weight:700}.admin-sidebar-brand p{color:#64748b;margin:.35rem 0 0;font-size:.75rem}.admin-nav{flex:1;padding:.75rem .65rem;overflow-y:auto}.admin-nav-group{margin-bottom:1.25rem}.admin-nav-group-label{letter-spacing:.1em;text-transform:uppercase;color:#475569;margin-bottom:.35rem;padding:0 .75rem;font-size:.65rem;font-weight:700}.admin-nav button{text-align:left;color:#94a3b8;cursor:pointer;background:0 0;border:none;border-radius:8px;align-items:center;gap:.65rem;width:100%;padding:.6rem .75rem;font-family:inherit;font-size:.875rem;font-weight:500;transition:all .15s;display:flex}.admin-nav button svg{opacity:.7;flex-shrink:0}.admin-nav button:hover{background:var(--admin-sidebar-hover);color:#f1f5f9}.admin-nav button.active{background:var(--admin-sidebar-active);color:#fff;box-shadow:inset 3px 0 0 var(--admin-gold)}.admin-nav button.active svg{color:var(--admin-gold);opacity:1}.admin-sidebar-footer{border-top:1px solid #ffffff0f;padding:1rem .65rem 1.25rem}.logout-btn{color:#94a3b8;cursor:pointer;background:#ffffff0a;border:1px solid #ffffff1a;border-radius:8px;justify-content:center;align-items:center;gap:.5rem;width:100%;padding:.6rem 1rem;font-family:inherit;font-size:.8rem;font-weight:600;transition:all .15s;display:flex}.logout-btn:hover{color:#fca5a5;background:#ef44441f;border-color:#ef44444d}.admin-main-wrap{margin-left:var(--sidebar-width);min-width:0;width:calc(100% - var(--sidebar-width));z-index:1;flex-direction:column;flex:1;display:flex;position:relative}.admin-topbar{z-index:30;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--admin-border);background:#f0f2f7d9;justify-content:space-between;align-items:center;gap:1rem;padding:.85rem 2rem;display:flex;position:sticky;top:0}.admin-topbar-title{color:var(--admin-text);margin:0;font-size:1.15rem;font-weight:700}.admin-topbar-sub{color:var(--admin-muted);margin:.15rem 0 0;font-size:.8rem}.admin-topbar-actions{align-items:center;gap:.65rem;display:flex}.admin-main{flex:1;width:100%;padding:1.75rem 2rem 2.5rem}.admin-card{background:var(--admin-surface);border-radius:var(--admin-radius-lg);box-shadow:var(--admin-shadow);border:1px solid var(--admin-border);padding:2rem}.admin-card h1{color:var(--admin-text);letter-spacing:-.02em;margin:0 0 .35rem;font-size:1.625rem;font-weight:800}.admin-card .subtitle{color:var(--admin-muted);margin:0 0 1.75rem;font-size:.9rem;line-height:1.5}.admin-page-header{margin-bottom:1.5rem}.dashboard-welcome{border-radius:var(--admin-radius-lg);color:#fff;background:linear-gradient(135deg,#0a1628 0%,#1e3a5f 100%);border:1px solid #ffaa0026;margin-bottom:1.75rem;padding:1.75rem 2rem}.dashboard-welcome h2{margin:0 0 .35rem;font-size:1.35rem;font-weight:700}.dashboard-welcome p{color:#94a3b8;margin:0;font-size:.875rem;line-height:1.5}.dashboard-meta{flex-wrap:wrap;gap:1rem;margin-top:1.25rem;display:flex}.dashboard-meta-item{color:#cbd5e1;background:#ffffff0f;border-radius:999px;align-items:center;gap:.4rem;padding:.35rem .75rem;font-size:.75rem;display:flex}.dashboard-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;display:grid}.dashboard-tile{background:var(--admin-surface);border:1px solid var(--admin-border);border-radius:var(--admin-radius);cursor:pointer;text-align:left;box-shadow:var(--admin-shadow);padding:1.25rem;font-family:inherit;transition:all .2s}.dashboard-tile:hover{border-color:var(--admin-gold-dim);box-shadow:var(--admin-shadow-lg);transform:translateY(-2px)}.dashboard-tile-icon{width:40px;height:40px;color:var(--admin-primary);background:linear-gradient(135deg,#1e40af1a 0%,#ffaa0014 100%);border-radius:10px;justify-content:center;align-items:center;margin-bottom:.85rem;display:flex}.dashboard-tile h3{color:var(--admin-text);margin:0 0 .25rem;font-size:.95rem;font-weight:700}.dashboard-tile p{color:var(--admin-muted);margin:0;font-size:.78rem;line-height:1.4}.btn-primary{color:#fff;cursor:pointer;background:linear-gradient(135deg,#1e40af 0%,#1d4ed8 100%);border:none;border-radius:8px;justify-content:center;align-items:center;gap:.4rem;padding:.65rem 1.35rem;font-family:inherit;font-size:.875rem;font-weight:600;transition:all .15s;display:inline-flex;box-shadow:0 2px 8px #1e40af40}.btn-primary:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px #1e40af59}.btn-primary:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn-secondary{background:var(--admin-surface);color:#475569;border:1px solid var(--admin-border);cursor:pointer;border-radius:8px;justify-content:center;align-items:center;gap:.4rem;padding:.65rem 1.25rem;font-family:inherit;font-size:.875rem;font-weight:600;transition:all .15s;display:inline-flex}.btn-secondary:hover{background:#f8fafc;border-color:#cbd5e1}.btn-ghost{color:var(--admin-muted);border:1px solid var(--admin-border);cursor:pointer;background:0 0;border-radius:8px;align-items:center;gap:.4rem;padding:.5rem .85rem;font-family:inherit;font-size:.8rem;font-weight:600;text-decoration:none;transition:all .15s;display:inline-flex}.btn-ghost:hover{color:var(--admin-primary);border-color:var(--admin-primary);background:#eff6ff}.save-bar{border-top:1px solid var(--admin-border);justify-content:flex-end;gap:.75rem;margin-top:2rem;padding-top:1.5rem;display:flex}.admin-editor-layout{grid-template-columns:minmax(0,1fr) minmax(340px,42%);align-items:start;gap:1.25rem;display:grid}.admin-editor-form{min-width:0}.admin-editor-placeholder{text-align:center;min-height:240px;color:var(--admin-muted);flex-direction:column;justify-content:center;align-items:center;gap:.85rem;display:flex}.admin-editor-placeholder p{max-width:360px;margin:0;font-size:.9rem;line-height:1.5}.admin-editor-placeholder svg{color:#dc2626}.admin-preview-panel{min-width:0;position:sticky;top:5.25rem}.admin-preview-panel-header{justify-content:space-between;align-items:flex-start;gap:.75rem;margin-bottom:.75rem;display:flex}.admin-preview-eyebrow{letter-spacing:.08em;text-transform:uppercase;color:var(--admin-primary);align-items:center;gap:.35rem;font-size:.72rem;font-weight:700;display:inline-flex}.admin-preview-panel-header h3{color:var(--admin-text);margin:.25rem 0 0;font-size:1rem;font-weight:700}.admin-preview-open{color:var(--admin-muted);border:1px solid var(--admin-border);background:var(--admin-surface);white-space:nowrap;border-radius:8px;align-items:center;gap:.35rem;padding:.4rem .7rem;font-size:.78rem;font-weight:600;text-decoration:none;display:inline-flex}.admin-preview-open:hover{color:var(--admin-primary);border-color:#1e40af40}.admin-preview-panel .admin-preview-shell{border:1px solid var(--admin-border);box-shadow:var(--admin-shadow-lg);background:#0f172a;border-radius:14px;overflow:hidden}.admin-preview-panel .admin-preview-chrome{background:#0f172a;border-bottom:1px solid #ffffff14;align-items:center;gap:.45rem;padding:.65rem .85rem;display:flex}.admin-preview-panel .admin-preview-dot{border-radius:50%;width:9px;height:9px}.admin-preview-panel .admin-preview-dot:first-child{background:#f87171}.admin-preview-panel .admin-preview-dot:nth-child(2){background:#fbbf24}.admin-preview-panel .admin-preview-dot:nth-child(3){background:#4ade80}.admin-preview-panel .admin-preview-url{color:#94a3b8;background:#020710a6;border:1px solid #ffffff0f;border-radius:999px;margin-left:.45rem;padding:.28rem .7rem;font-size:.7rem}.admin-preview-panel .admin-preview-viewport{isolation:isolate;background:#020710;width:100%;position:relative;overflow:hidden}.admin-preview-stage,.login-preview-stage{max-width:100%;position:relative;overflow:hidden}.admin-preview-panel .admin-preview-iframe{transform-origin:0 0;pointer-events:auto;background:#fff;border:0;position:absolute;top:0;left:0}.admin-preview-loading{color:#94a3b8;z-index:2;background:#020710;flex-direction:column;justify-content:center;align-items:center;gap:.75rem;font-size:.85rem;display:flex;position:absolute;inset:0}.admin-preview-note{color:var(--admin-muted);margin:.65rem 0 0;font-size:.75rem;line-height:1.4}.form-field{margin-bottom:1.25rem}.form-label{color:#334155;letter-spacing:.02em;margin-bottom:.4rem;font-size:.78rem;font-weight:700;display:block}.form-input,.form-textarea{border:1px solid var(--admin-border);width:100%;color:var(--admin-text);background:#fafbfc;border-radius:8px;padding:.7rem .85rem;font-family:inherit;font-size:.9rem;transition:border-color .15s,box-shadow .15s,background .15s}.form-input:focus,.form-textarea:focus{border-color:var(--admin-primary);background:#fff;outline:none;box-shadow:0 0 0 3px #1e40af1f}.form-textarea{resize:vertical;min-height:96px;line-height:1.5}.form-preview{border:1px solid var(--admin-border);background:#f1f5f9;border-radius:10px;max-height:140px;margin-bottom:.65rem;overflow:hidden}.form-preview img,.form-preview video{object-fit:cover;width:100%;max-height:140px;display:block}.form-upload-btn{color:#475569;cursor:pointer;background:#f1f5f9;border:1px dashed #cbd5e1;border-radius:8px;align-items:center;gap:.4rem;margin-top:.5rem;padding:.45rem 1rem;font-size:.8rem;font-weight:600;transition:all .15s;display:inline-flex}.form-upload-btn:hover{border-color:var(--admin-primary);color:var(--admin-primary);background:#eff6ff}.array-section{border-top:1px solid var(--admin-border);margin-top:2rem;padding-top:1.5rem}.array-header{justify-content:space-between;align-items:center;margin-bottom:1rem;display:flex}.array-title{color:var(--admin-text);margin:0;font-size:1rem;font-weight:700}.array-add-btn{background:var(--admin-primary);color:#fff;cursor:pointer;border:none;border-radius:8px;align-items:center;gap:.35rem;padding:.4rem .85rem;font-family:inherit;font-size:.8rem;font-weight:600;transition:background .15s;display:inline-flex}.array-add-btn:hover{background:var(--admin-primary-hover)}.array-item{border-radius:var(--admin-radius);border:1px solid var(--admin-border);background:#f8fafc;margin-bottom:1rem;padding:1.25rem}.array-item-header{border-bottom:1px solid var(--admin-border);justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;display:flex}.array-item-badge{color:var(--admin-primary);background:#eff6ff;border-radius:999px;padding:.2rem .6rem;font-size:.75rem;font-weight:700}.array-remove-btn{color:#dc2626;cursor:pointer;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;align-items:center;gap:.3rem;padding:.3rem .65rem;font-family:inherit;font-size:.75rem;font-weight:600;transition:all .15s;display:inline-flex}.array-remove-btn:hover{background:#fee2e2}.login-page{grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);min-height:100vh;display:grid}.login-preview{background:linear-gradient(160deg,#020710 0%,#0a1628 40%,#1e3a5f 100%);flex-direction:column;gap:1.25rem;min-height:100vh;padding:2rem 2rem 2.25rem;display:flex;position:relative;overflow:hidden}.login-preview:before{content:"";pointer-events:none;background:radial-gradient(circle,#ffaa001f 0%,#0000 70%);width:400px;height:400px;position:absolute;top:-20%;right:-10%}.login-preview-intro{z-index:1;align-items:flex-start;gap:1rem;display:flex;position:relative}.login-preview-intro img{flex-shrink:0;height:40px}.login-preview-intro h1{color:#fff;letter-spacing:-.03em;margin:0 0 .35rem;font-size:1.5rem;font-weight:800}.login-preview-intro h1 span{color:var(--admin-gold)}.login-preview-intro p{color:#94a3b8;max-width:420px;margin:0;font-size:.85rem;line-height:1.5}.login-preview-link{color:#cbd5e1;white-space:nowrap;background:#ffffff0a;border:1px solid #ffffff1f;border-radius:999px;align-items:center;gap:.4rem;margin-left:auto;padding:.5rem .85rem;font-size:.8rem;font-weight:600;text-decoration:none;transition:all .15s;display:inline-flex}.login-preview-link:hover{color:#fff;background:#ffaa0014;border-color:#ffaa0059}.login-preview-shell{z-index:1;background:#0f172a;border:1px solid #ffffff1a;border-radius:14px;flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden;box-shadow:0 24px 64px #02071073}.login-preview-chrome{background:#0f172af2;border-bottom:1px solid #ffffff14;align-items:center;gap:.45rem;padding:.7rem .9rem;display:flex}.login-preview-dot{background:#94a3b859;border-radius:50%;width:10px;height:10px}.login-preview-dot:first-child{background:#f87171}.login-preview-dot:nth-child(2){background:#fbbf24}.login-preview-dot:nth-child(3){background:#4ade80}.login-preview-url{color:#94a3b8;background:#020710a6;border:1px solid #ffffff0f;border-radius:999px;margin-left:.5rem;padding:.3rem .75rem;font-size:.72rem}.login-preview-viewport{isolation:isolate;background:#020710;width:100%;position:relative;overflow:hidden}.login-preview-fallback{text-align:center;color:#64748b;background:linear-gradient(160deg,#0f172a 0%,#1e293b 55%,#0f172a 100%);flex-direction:column;justify-content:center;align-items:center;gap:12px;height:100%;min-height:220px;padding:24px;display:flex}.login-preview-fallback p{color:#94a3b8;max-width:280px;margin:0;font-size:.9rem;line-height:1.45}.login-preview-iframe{transform-origin:0 0;pointer-events:auto;background:#fff;border:0;position:absolute;top:0;left:0}.login-form-side{background:var(--admin-bg);justify-content:center;align-items:center;padding:2rem;display:flex}.login-card{background:var(--admin-surface);border-radius:var(--admin-radius-lg);width:100%;max-width:400px;box-shadow:var(--admin-shadow-lg);border:1px solid var(--admin-border);padding:2.5rem}.login-card h2{color:var(--admin-text);margin:0 0 .25rem;font-size:1.35rem;font-weight:800}.login-card .login-subtitle{color:var(--admin-muted);margin:0 0 1.75rem;font-size:.875rem}.login-card .form-field{margin-bottom:1rem}.login-card .btn-primary{width:100%;margin-top:.5rem;padding:.8rem;font-size:.95rem}.login-error{color:#dc2626;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;align-items:center;gap:.5rem;margin-bottom:1rem;padding:.65rem .85rem;font-size:.8rem;display:flex}.toast{z-index:100;box-shadow:var(--admin-shadow-lg);border-radius:10px;align-items:center;gap:.5rem;padding:.85rem 1.25rem;font-size:.875rem;font-weight:600;animation:.3s slideIn;display:flex;position:fixed;bottom:1.5rem;right:1.5rem}.toast.success{color:#166534;background:#fff;border:1px solid #bbf7d0}.toast.error{color:#dc2626;background:#fff;border:1px solid #fecaca}@keyframes slideIn{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}.admin-loading{background:var(--admin-bg);min-height:100vh;color:var(--admin-muted);flex-direction:column;justify-content:center;align-items:center;gap:1rem;display:flex}.admin-spinner{border:3px solid var(--admin-border);border-top-color:var(--admin-primary);border-radius:50%;width:36px;height:36px;animation:.7s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.settings-card{max-width:480px}.msg-success{color:#166534;margin:.5rem 0;font-size:.875rem}.msg-error{color:#dc2626;margin:.5rem 0;font-size:.875rem}.admin-mobile-toggle{z-index:70;background:var(--admin-sidebar);color:#fff;cursor:pointer;border:1px solid #ffffff1a;border-radius:8px;justify-content:center;align-items:center;width:42px;height:42px;display:none;position:fixed;top:.85rem;left:.85rem}.admin-sidebar-overlay{z-index:55;background:#02071099;display:none;position:fixed;inset:0}@media (width>=901px){.admin-sidebar-overlay{display:none!important}.admin-sidebar{transform:none!important}}@media (width<=900px){.login-page{grid-template-columns:1fr}.login-preview{gap:.85rem;min-height:auto;padding:1.25rem 1rem 1rem}.login-preview-intro{flex-wrap:wrap}.login-preview-intro h1{font-size:1.25rem}.login-preview-link{margin-left:0}.login-preview-shell{min-height:220px}.admin-mobile-toggle{display:flex}.admin-sidebar{transition:transform .25s;transform:translate(-100%)}.admin-sidebar.open{transform:translate(0)}.admin-sidebar-overlay.visible{display:block}.admin-main-wrap{width:100%;margin-left:0}.admin-topbar{padding:.85rem 1rem .85rem 3.5rem}.admin-main{padding:1.25rem 1rem 2rem}.dashboard-grid,.admin-editor-layout{grid-template-columns:1fr}.admin-preview-panel{order:-1;position:static}} diff --git a/dist/uploads-proxy.php b/dist/uploads-proxy.php new file mode 100644 index 0000000..c64afd0 --- /dev/null +++ b/dist/uploads-proxy.php @@ -0,0 +1,66 @@ + 'Missing file', 'code' => 'VALIDATION']); + exit; +} + +$url = rtrim($apiOrigin, '/') . '/uploads/' . str_replace(' ', '%20', $file); + +if (!function_exists('curl_init')) { + http_response_code(500); + exit('curl required'); +} + +$ch = curl_init($url); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 60, +]); + +$response = curl_exec($ch); +if ($response === false) { + curl_close($ch); + http_response_code(502); + exit('Upload server unreachable'); +} + +$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); +$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE); +curl_close($ch); + +http_response_code($status > 0 ? $status : 502); +$rawHeaders = substr($response, 0, $headerSize); +$body = substr($response, $headerSize); + +$skip = ['transfer-encoding', 'connection', 'keep-alive']; +foreach (explode("\r\n", $rawHeaders) as $line) { + if ($line === '' || stripos($line, 'HTTP/') === 0) { + continue; + } + $parts = explode(':', $line, 2); + if (count($parts) < 2) { + continue; + } + $name = trim($parts[0]); + if (in_array(strtolower($name), $skip, true)) { + continue; + } + header($name . ':' . $parts[1], false); +} + +echo $body; diff --git a/public/.htaccess b/public/.htaccess index 0967df7..97dff0b 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -1,15 +1,30 @@ # Deployed under /citpl_website/ on Apache. -# Requires: mod_rewrite, mod_headers, and (for API) mod_proxy + mod_proxy_http +# Requires: mod_rewrite, mod_headers, PHP curl +# API: PHP proxy to Node on 127.0.0.1:3001 (start with: npm run server / pm2) -RewriteEngine On + + RewriteEngine On + RewriteBase /citpl_website/ -# Proxy Node API + uploads (start server with: node server/index.js) -RewriteRule ^api/(.*)$ http://127.0.0.1:3001/api/$1 [P,L] -RewriteRule ^uploads/(.*)$ http://127.0.0.1:3001/uploads/$1 [P,L] + # API -> PHP proxy (works without mod_proxy) + RewriteRule ^api(?:/.*)?$ api/index.php [QSA,L] -# Allow admin login/section preview to iframe this page (same origin) - - Header always unset X-Frame-Options - Header always unset Content-Security-Policy - Header always set Content-Security-Policy "frame-ancestors 'self'" - + # Uploads -> PHP proxy + RewriteRule ^uploads/(.*)$ uploads-proxy.php?file=$1 [QSA,L] + + +# Host security headers use frame-ancestors 'none' + X-Frame-Options DENY. +# Edit them so admin can embed preview.html (same origin). + + Header always edit Content-Security-Policy "frame-ancestors 'none'" "frame-ancestors 'self'" + Header always edit X-Frame-Options "DENY" "SAMEORIGIN" + + + Header unset X-Frame-Options + Header always unset X-Frame-Options + Header unset Content-Security-Policy + Header always unset Content-Security-Policy + Header always set X-Frame-Options "SAMEORIGIN" + Header always set Content-Security-Policy "frame-ancestors 'self'" + + diff --git a/public/api/index.php b/public/api/index.php new file mode 100644 index 0000000..035169d --- /dev/null +++ b/public/api/index.php @@ -0,0 +1,119 @@ + $value) { + $lower = strtolower((string) $name); + if ($lower === 'host' || $lower === 'content-length') { + continue; + } + $headers[] = $name . ': ' . $value; + } +} else { + foreach ($_SERVER as $key => $value) { + if (strpos($key, 'HTTP_') !== 0) { + continue; + } + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + if (strtolower($name) === 'host') { + continue; + } + $headers[] = $name . ': ' . $value; + } + if (!empty($_SERVER['CONTENT_TYPE'])) { + $headers[] = 'Content-Type: ' . $_SERVER['CONTENT_TYPE']; + } +} + +if (!function_exists('curl_init')) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode([ + 'error' => 'PHP curl extension is required for the API proxy', + 'code' => 'PROXY_MISCONFIGURED', + ]); + exit; +} + +$ch = curl_init($url); +curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 60, + CURLOPT_HTTPHEADER => $headers, +]); + +if ($method !== 'GET' && $method !== 'HEAD') { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); +} + +$response = curl_exec($ch); +if ($response === false) { + $err = curl_error($ch); + curl_close($ch); + http_response_code(502); + header('Content-Type: application/json'); + echo json_encode([ + 'error' => 'API server unreachable. On the host run: npm run server (port 3001)', + 'detail' => $err, + 'code' => 'BAD_GATEWAY', + ]); + exit; +} + +$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); +$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE); +curl_close($ch); + +$rawHeaders = substr($response, 0, $headerSize); +$responseBody = substr($response, $headerSize); + +http_response_code($status > 0 ? $status : 502); + +$skip = ['transfer-encoding', 'connection', 'keep-alive', 'content-length']; +foreach (explode("\r\n", $rawHeaders) as $line) { + if ($line === '' || stripos($line, 'HTTP/') === 0) { + continue; + } + $parts = explode(':', $line, 2); + if (count($parts) < 2) { + continue; + } + $name = trim($parts[0]); + if (in_array(strtolower($name), $skip, true)) { + continue; + } + header($name . ':' . $parts[1], false); +} + +echo $responseBody; diff --git a/public/uploads-proxy.php b/public/uploads-proxy.php new file mode 100644 index 0000000..c64afd0 --- /dev/null +++ b/public/uploads-proxy.php @@ -0,0 +1,66 @@ + 'Missing file', 'code' => 'VALIDATION']); + exit; +} + +$url = rtrim($apiOrigin, '/') . '/uploads/' . str_replace(' ', '%20', $file); + +if (!function_exists('curl_init')) { + http_response_code(500); + exit('curl required'); +} + +$ch = curl_init($url); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 60, +]); + +$response = curl_exec($ch); +if ($response === false) { + curl_close($ch); + http_response_code(502); + exit('Upload server unreachable'); +} + +$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); +$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE); +curl_close($ch); + +http_response_code($status > 0 ? $status : 502); +$rawHeaders = substr($response, 0, $headerSize); +$body = substr($response, $headerSize); + +$skip = ['transfer-encoding', 'connection', 'keep-alive']; +foreach (explode("\r\n", $rawHeaders) as $line) { + if ($line === '' || stripos($line, 'HTTP/') === 0) { + continue; + } + $parts = explode(':', $line, 2); + if (count($parts) < 2) { + continue; + } + $name = trim($parts[0]); + if (in_array(strtolower($name), $skip, true)) { + continue; + } + header($name . ':' . $parts[1], false); +} + +echo $body; diff --git a/server/index.js b/server/index.js index bc7e6c2..5ce2c95 100644 --- a/server/index.js +++ b/server/index.js @@ -33,6 +33,14 @@ app.use('/api/auth', authRoutes); app.use('/api/content', contentRoutes); app.use('/api/upload', uploadRoutes); +// Optional: serve Vite dist from Node (set SERVE_STATIC=1). Useful when Apache +// proxies the whole /citpl_website path to this process. +if (process.env.SERVE_STATIC === '1') { + const distDir = path.join(__dirname, '..', 'dist'); + app.use(express.static(distDir)); + app.use('/citpl_website', express.static(distDir)); +} + app.use((err, _req, res, _next) => { if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ error: 'File too large (max 50MB)', code: 'FILE_TOO_LARGE' }); diff --git a/src/admin/App.jsx b/src/admin/App.jsx index c3f00da..278fa79 100644 --- a/src/admin/App.jsx +++ b/src/admin/App.jsx @@ -73,9 +73,12 @@ function Toast({ message, type }) { function LoginPreview() { const viewportRef = useRef(null); + const iframeRef = useRef(null); const [scale, setScale] = useState(0.5); + const [frameBlocked, setFrameBlocked] = useState(false); const previewWidth = 1280; const previewHeight = 800; + const previewSrc = `${BASE}preview.html`; useEffect(() => { const el = viewportRef.current; @@ -91,6 +94,22 @@ function LoginPreview() { observer.observe(el); return () => observer.disconnect(); }, []); + + // Host CSP (frame-ancestors 'none' / XFO DENY) can block the embed — show a fallback. + useEffect(() => { + const timer = setTimeout(() => { + try { + const doc = iframeRef.current?.contentDocument; + if (!doc?.body || doc.body.childElementCount === 0) { + setFrameBlocked(true); + } + } catch { + setFrameBlocked(true); + } + }, 2000); + return () => clearTimeout(timer); + }, []); + return (
@@ -116,21 +135,32 @@ function LoginPreview() { ref={viewportRef} style={{ height: previewHeight * scale }} > -
-