[object Object]

← back to Prestige Car Wash Video

Add PrestigeAdmin composition: dedicated 68s Growth Center backend walkthrough

322c8af0293d0422c94ec63f5c1dd70d7a26624f · 2026-07-12 11:17:43 -0700 · Steve Abrams

Files touched

Diff

commit 322c8af0293d0422c94ec63f5c1dd70d7a26624f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 11:17:43 2026 -0700

    Add PrestigeAdmin composition: dedicated 68s Growth Center backend walkthrough
---
 src/AdminVideo.tsx   | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/Root.tsx         | 10 ++++++++
 src/adminSections.ts | 37 ++++++++++++++++++++++++++++
 3 files changed, 115 insertions(+)

diff --git a/src/AdminVideo.tsx b/src/AdminVideo.tsx
new file mode 100644
index 0000000..e8f5267
--- /dev/null
+++ b/src/AdminVideo.tsx
@@ -0,0 +1,68 @@
+import { AbsoluteFill, Sequence, Audio, staticFile, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
+import { ADMIN_SECTIONS } from './adminSections';
+import { Background } from './Background';
+import { BrowserShot } from './BrowserShot';
+import { Caption } from './Caption';
+import { Card } from './Card';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+const FadeWrap: React.FC<{ dur: number; children: React.ReactNode }> = ({ dur, children }) => {
+  const f = useCurrentFrame();
+  const o = interpolate(f, [0, 10, dur - 9, dur], [0, 1, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
+  return <AbsoluteFill style={{ opacity: o }}>{children}</AbsoluteFill>;
+};
+
+export const AdminVideo: React.FC = () => {
+  const frame = useCurrentFrame();
+  const { durationInFrames } = useVideoConfig();
+  const offsets: number[] = [];
+  let acc = 0;
+  for (const s of ADMIN_SECTIONS) { offsets.push(acc); acc += s.frames; }
+  let active = 0;
+  for (let i = 0; i < ADMIN_SECTIONS.length; i++) if (frame >= offsets[i]) active = i;
+  const accent = ADMIN_SECTIONS[active].accent;
+  const shotIndex = ADMIN_SECTIONS.slice(0, active + 1).filter((s) => s.kind === 'shot').length;
+  const shotTotal = ADMIN_SECTIONS.filter((s) => s.kind === 'shot').length;
+
+  return (
+    <AbsoluteFill style={{ backgroundColor: '#060c16' }}>
+      <Background accent={accent} />
+
+      {ADMIN_SECTIONS.map((s, i) => (
+        <Sequence key={s.id} from={offsets[i]} durationInFrames={s.frames} name={s.title}>
+          <FadeWrap dur={s.frames}>
+            {s.kind === 'card' ? (
+              <>
+                <Card s={s as any} />
+                {s.id === 'intro' && <Audio src={staticFile('audio/admin.wav')} />}
+              </>
+            ) : (
+              <>
+                <BrowserShot s={s as any} />
+                <Caption s={s as any} index={i} total={ADMIN_SECTIONS.length} />
+              </>
+            )}
+          </FadeWrap>
+        </Sequence>
+      ))}
+
+      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: 'rgba(255,255,255,0.08)' }}>
+        <div style={{ height: 6, width: `${(frame / durationInFrames) * 100}%`, background: accent, boxShadow: `0 0 16px ${accent}` }} />
+      </div>
+
+      {ADMIN_SECTIONS[active].kind === 'shot' && (
+        <div style={{ position: 'absolute', top: 34, right: 54, fontFamily: FONT, fontWeight: 700, fontSize: 24, color: '#c9a24a', letterSpacing: 2 }}>
+          {String(shotIndex).padStart(2, '0')}<span style={{ opacity: 0.4 }}> / {String(shotTotal).padStart(2, '0')}</span>
+        </div>
+      )}
+
+      <div style={{ position: 'absolute', top: 30, left: 54, display: 'flex', alignItems: 'center', gap: 12,
+        fontFamily: FONT, fontWeight: 800, fontSize: 24, color: '#e8f1ff', letterSpacing: 0.5 }}>
+        <span style={{ display: 'inline-flex', width: 34, height: 34, borderRadius: 9,
+          background: 'linear-gradient(135deg,#f5b301,#d97706)', alignItems: 'center', justifyContent: 'center', fontSize: 18 }}>🚗</span>
+        Prestige<span style={{ opacity: 0.5, fontWeight: 600 }}>Admin</span>
+      </div>
+    </AbsoluteFill>
+  );
+};
diff --git a/src/Root.tsx b/src/Root.tsx
index ffc4935..02dcf22 100644
--- a/src/Root.tsx
+++ b/src/Root.tsx
@@ -1,12 +1,22 @@
 import { Composition } from 'remotion';
 import { PrestigeShowcase } from './Video';
 import { StoryVideo } from './StoryVideo';
+import { AdminVideo } from './AdminVideo';
 import { FPS, WIDTH, HEIGHT, TOTAL_FRAMES } from './sections';
 import { TOTAL as STORY_TOTAL } from './story';
+import { ADMIN_TOTAL } from './adminSections';
 
 export const RemotionRoot: React.FC = () => {
   return (
     <>
+      <Composition
+        id="PrestigeAdmin"
+        component={AdminVideo}
+        durationInFrames={ADMIN_TOTAL}
+        fps={FPS}
+        width={WIDTH}
+        height={HEIGHT}
+      />
       <Composition
         id="PrestigeStory"
         component={StoryVideo}
diff --git a/src/adminSections.ts b/src/adminSections.ts
new file mode 100644
index 0000000..6b8b64a
--- /dev/null
+++ b/src/adminSections.ts
@@ -0,0 +1,37 @@
+import dims from './dims.json';
+import type { Section } from './sections';
+import { FPS } from './sections';
+
+const AMBER = '#f5b301';
+const D: Record<string, { w: number; h: number }> = dims as any;
+const sec = (s: number) => Math.round(s * FPS);
+
+const TABS: Array<[string, string, string]> = [
+  ['overview',    'Overview',              'Live counts across the whole growth dataset'],
+  ['competitors', 'Competitors',           '12 rival washes in the San Fernando Valley'],
+  ['best-post',   'Best Times to Post',    'When engagement peaks by channel'],
+  ['best-market', 'Best Times to Market',  'When to spend to convert'],
+  ['holidays',    'Holiday-Driven',        'Eight seasonal campaign hooks'],
+  ['services',    'Services',              'Editable service catalog behind the storefront'],
+  ['suggestions', 'Grow the Business',     'Ten ranked growth plays with rationale'],
+  ['ads',         'Online Ads',            'Platform-by-platform ad headlines'],
+  ['directories', 'Groups · Directories',  'Nextdoor, local groups & listing sites'],
+  ['google',      'Google Business',       'Places profile draft-update panel'],
+  ['leads',       'Leads',                 'Inbound booking requests from the site'],
+  ['credentials', 'Credentials',           'Keys & access (presence + last-4 only)'],
+];
+
+export const ADMIN_SECTIONS: Section[] = [
+  { kind: 'card', id: 'intro', act: 'Behind the scenes', title: 'Growth Center',
+    sub: 'The admin backend that keeps Prestige sharp', frames: sec(8), accent: AMBER },
+  ...TABS.map(([key, title, sub]): Section => {
+    const dk = 'admin-' + key;
+    return { kind: 'shot', id: dk, file: `shots/${dk}.png`, w: D[dk]?.w, h: D[dk]?.h,
+      act: 'Admin · Growth Center', title, sub, url: 'prestigecarwash.com/admin',
+      frames: sec(4.6), accent: AMBER };
+  }),
+  { kind: 'card', id: 'outro', act: 'Prestige Car Wash', title: 'Growth Center',
+    sub: 'Competitors · campaigns · ads · leads — all in one place', frames: sec(4.5), accent: '#34d399' },
+];
+
+export const ADMIN_TOTAL = ADMIN_SECTIONS.reduce((a, s) => a + s.frames, 0);

← 12ff853 chore: lint, refactor (dead-code), v1.0.1 (session close)  ·  back to Prestige Car Wash Video  ·  (newest)