@nextblock-cms/db 0.13.9 → 0.13.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,52 +1,52 @@
1
- -- 00000000000009_home_live_demo_promo_contrast.sql
2
- -- Restyles the "Live Demo" promo that migrations 006/007 seeded on the EN (slug 'home')
3
- -- and FR (slug 'accueil') home pages. 006/007 gave it a fixed dark-navy GRADIENT section
4
- -- background -- and a gradient background is an inline style, so it renders identically
5
- -- in light and dark theme. Sitting directly under the (also dark) hero, that made the
6
- -- first two sections both dark in light mode and killed the old dark-hero -> white-second
7
- -- contrast rhythm.
8
- --
9
- -- Fix (append-only, do NOT edit 006/007 -- both already applied to prod + sandbox):
10
- -- switch the section background to "none" (transparent -> inherits the theme-aware page
11
- -- background: light in light theme, dark in dark theme) and render the promo as a single
12
- -- self-contained ELEVATED PANEL. The panel is a soft light card in light theme and keeps
13
- -- the rich navy look in dark theme (dark: gradient), with the browser mockup deliberately
14
- -- kept dark in both themes as a "product screenshot" focal point. The <!--nb-sandbox-promo-->
15
- -- sentinel is preserved so the reset-sandbox route still strips it on cms.nextblock.dev.
16
- --
17
- -- Idempotent: re-running sets identical content. Targets exactly the promo blocks via the
18
- -- nb-sandbox-promo sentinel, scoped by language (mirrors 00000000000007). blocks.content is
19
- -- JSONB, written with dollar-quoting.
20
- --
21
- -- NOTE: keep the word "sandbox" OUT of this filename -- generate-sandbox-reset.ts excludes
22
- -- any migration whose filename contains "sandbox" from the sandbox reset bundle.
23
-
24
- DO $body$
25
- DECLARE
26
- v_en_lang integer;
27
- v_fr_lang integer;
28
- BEGIN
29
- SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;
30
- SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;
31
-
32
- UPDATE public.blocks
33
- SET content = $en$
34
- {"container_type":"container","background":{"type":"none"},"responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"column_gap":"xl","vertical_alignment":"center","padding":{"top":"xl","bottom":"xl"},"column_blocks":[[{"block_type":"text","content":{"html_content":"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets daily, so explore freely and break whatever you like.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.ca</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; Resets daily</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with a full checkout flow.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean daily</strong> &mdash; always a fresh start.</span></li></ul></div></div></div></div>"}}]]}
35
- $en$::jsonb,
36
- updated_at = now()
37
- WHERE block_type = 'section'
38
- AND page_id IS NOT NULL
39
- AND language_id = v_en_lang
40
- AND content::text LIKE '%nb-sandbox-promo%';
41
-
42
- UPDATE public.blocks
43
- SET content = $fr$
44
- {"container_type":"container","background":{"type":"none"},"responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"column_gap":"xl","vertical_alignment":"center","padding":{"top":"xl","bottom":"xl"},"column_blocks":[[{"block_type":"text","content":{"html_content":"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire des articles sur NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque jour : explorez librement et cassez tout ce que vous voulez.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.ca</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque jour</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec un parcours de paiement complet.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque jour</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div></div></div>"}}]]}
45
- $fr$::jsonb,
46
- updated_at = now()
47
- WHERE block_type = 'section'
48
- AND page_id IS NOT NULL
49
- AND language_id = v_fr_lang
50
- AND content::text LIKE '%nb-sandbox-promo%';
51
- END
52
- $body$;
1
+ -- 00000000000009_home_live_demo_promo_contrast.sql
2
+ -- Restyles the "Live Demo" promo that migrations 006/007 seeded on the EN (slug 'home')
3
+ -- and FR (slug 'accueil') home pages. 006/007 gave it a fixed dark-navy GRADIENT section
4
+ -- background -- and a gradient background is an inline style, so it renders identically
5
+ -- in light and dark theme. Sitting directly under the (also dark) hero, that made the
6
+ -- first two sections both dark in light mode and killed the old dark-hero -> white-second
7
+ -- contrast rhythm.
8
+ --
9
+ -- Fix (append-only, do NOT edit 006/007 -- both already applied to prod + sandbox):
10
+ -- switch the section background to "none" (transparent -> inherits the theme-aware page
11
+ -- background: light in light theme, dark in dark theme) and render the promo as a single
12
+ -- self-contained ELEVATED PANEL. The panel is a soft light card in light theme and keeps
13
+ -- the rich navy look in dark theme (dark: gradient), with the browser mockup deliberately
14
+ -- kept dark in both themes as a "product screenshot" focal point. The <!--nb-sandbox-promo-->
15
+ -- sentinel is preserved so the reset-sandbox route still strips it on cms.nextblock.dev.
16
+ --
17
+ -- Idempotent: re-running sets identical content. Targets exactly the promo blocks via the
18
+ -- nb-sandbox-promo sentinel, scoped by language (mirrors 00000000000007). blocks.content is
19
+ -- JSONB, written with dollar-quoting.
20
+ --
21
+ -- NOTE: keep the word "sandbox" OUT of this filename -- generate-sandbox-reset.ts excludes
22
+ -- any migration whose filename contains "sandbox" from the sandbox reset bundle.
23
+
24
+ DO $body$
25
+ DECLARE
26
+ v_en_lang integer;
27
+ v_fr_lang integer;
28
+ BEGIN
29
+ SELECT id INTO v_en_lang FROM public.languages WHERE code = 'en' LIMIT 1;
30
+ SELECT id INTO v_fr_lang FROM public.languages WHERE code = 'fr' LIMIT 1;
31
+
32
+ UPDATE public.blocks
33
+ SET content = $en$
34
+ {"container_type":"container","background":{"type":"none"},"responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"column_gap":"xl","vertical_alignment":"center","padding":{"top":"xl","bottom":"xl"},"column_blocks":[[{"block_type":"text","content":{"html_content":"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>Live Demo &middot; No Signup</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Try the full CMS,<br/>live right now.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Don't just read about NextBlock&trade; &mdash; take the wheel. Our public sandbox is a complete, pre-loaded install where you can build pages with the block editor, manage bilingual content, and browse a working demo store. It resets daily, so explore freely and break whatever you like.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>Explore the Live Demo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Open the CMS Dashboard &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Demo login <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.ca</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; Resets daily</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>A living NextBlock&trade; install &mdash; everything here is real and clickable:</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Full block editor</strong> with slash commands &amp; drag-and-drop.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Bilingual</strong> English / French content management.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>A complete demo storefront</strong> with a full checkout flow.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Wipes clean daily</strong> &mdash; always a fresh start.</span></li></ul></div></div></div></div>"}}]]}
35
+ $en$::jsonb,
36
+ updated_at = now()
37
+ WHERE block_type = 'section'
38
+ AND page_id IS NOT NULL
39
+ AND language_id = v_en_lang
40
+ AND content::text LIKE '%nb-sandbox-promo%';
41
+
42
+ UPDATE public.blocks
43
+ SET content = $fr$
44
+ {"container_type":"container","background":{"type":"none"},"responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"column_gap":"xl","vertical_alignment":"center","padding":{"top":"xl","bottom":"xl"},"column_blocks":[[{"block_type":"text","content":{"html_content":"<!--nb-sandbox-promo--><div class='relative overflow-hidden rounded-[2.5rem] border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-blue-50 shadow-2xl shadow-slate-900/10 dark:border-white/10 dark:from-slate-900 dark:via-slate-900 dark:to-blue-950 dark:shadow-black/40'><div class='grid gap-10 p-8 sm:p-12 lg:grid-cols-2 lg:items-center'><div><p class='text-xs uppercase tracking-[0.25em] text-cyan-600 dark:text-cyan-400 font-bold mb-4'>D&eacute;mo en direct &middot; Sans inscription</p><h2 class='text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-6 leading-tight'>Essayez le CMS complet,<br/>en direct d&egrave;s maintenant.</h2><p class='text-lg text-slate-600 dark:text-slate-300 max-w-xl leading-relaxed mb-8'>Ne vous contentez pas de lire des articles sur NextBlock&trade; &mdash; prenez les commandes. Notre bac &agrave; sable public est une installation compl&egrave;te et pr&eacute;charg&eacute;e o&ugrave; vous pouvez cr&eacute;er des pages avec l'&eacute;diteur de blocs, g&eacute;rer du contenu bilingue et parcourir une vraie boutique de d&eacute;monstration. Il se r&eacute;initialise chaque jour : explorez librement et cassez tout ce que vous voulez.</p><div class='flex flex-col sm:flex-row gap-4'><a href='https://cms.nextblock.dev' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/25 transition-all no-underline'>D&eacute;couvrir la d&eacute;mo &rarr;</a><a href='https://cms.nextblock.dev/sign-in' target='_blank' rel='noopener noreferrer' class='inline-flex items-center justify-center gap-2 rounded-xl px-7 h-12 text-sm font-semibold text-slate-700 dark:text-white border border-slate-300 dark:border-white/20 hover:bg-slate-100 dark:hover:bg-white/10 transition-all no-underline'>Ouvrir le tableau de bord &rarr;</a></div><p class='text-xs text-slate-500 dark:text-slate-400 mt-4'>Identifiants d&eacute;mo <span class='font-mono text-slate-700 dark:text-slate-300'>demo@nextblock.ca</span> / <span class='font-mono text-slate-700 dark:text-slate-300'>password</span> &middot; R&eacute;initialis&eacute; chaque jour</p></div><div class='rounded-3xl overflow-hidden border border-slate-800 bg-slate-900 shadow-xl shadow-slate-900/20 dark:border-white/10 dark:bg-white/5 dark:backdrop-blur-xl'><div class='flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5'><span class='w-3 h-3 rounded-full bg-red-400/70'></span><span class='w-3 h-3 rounded-full bg-yellow-400/70'></span><span class='w-3 h-3 rounded-full bg-green-400/70'></span><span class='ml-3 flex-1 truncate rounded-md bg-black/30 px-3 py-1 text-xs font-mono text-slate-300'>cms.nextblock.dev</span></div><div class='p-6 sm:p-8'><p class='text-sm text-slate-300 mb-5'>Une installation NextBlock&trade; bien vivante &mdash; tout ici est r&eacute;el et cliquable :</p><ul class='space-y-4 text-sm text-slate-200'><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>&Eacute;diteur de blocs complet</strong> : commandes slash et glisser-d&eacute;poser.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Gestion bilingue</strong> du contenu fran&ccedil;ais / anglais.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Une boutique de d&eacute;monstration compl&egrave;te</strong> avec un parcours de paiement complet.</span></li><li class='flex items-start gap-3'><span class='mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-500/15 text-cyan-300 text-xs'>&#10003;</span><span><strong class='text-white'>Remis &agrave; z&eacute;ro chaque jour</strong> &mdash; toujours un nouveau d&eacute;part.</span></li></ul></div></div></div></div>"}}]]}
45
+ $fr$::jsonb,
46
+ updated_at = now()
47
+ WHERE block_type = 'section'
48
+ AND page_id IS NOT NULL
49
+ AND language_id = v_fr_lang
50
+ AND content::text LIKE '%nb-sandbox-promo%';
51
+ END
52
+ $body$;
@@ -1,79 +1,79 @@
1
- -- Drop the github_username profile column and remove the GitHub-auth artifacts.
2
- --
3
- -- GitHub OAuth sign-in / account-linking was removed from the app: it cannot be
4
- -- provisioned as part of a one-click deployment (each install needs its own GitHub
5
- -- OAuth app + client secret + Supabase's manual-linking toggle, none of which can
6
- -- ship in a migration). github_username was only ever populated from that OAuth
7
- -- flow, so the column and its seed strings are now dead.
8
- --
9
- -- NOTE: the self-update "Connect GitHub" flow (lib/updates/*, ConnectGitHubButton)
10
- -- is a separate feature and is intentionally left untouched — the connect_github
11
- -- translation is kept for it.
12
-
13
- -- 1. Rewrite handle_new_user() so new signups no longer read/populate github_username.
14
- CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger
15
- LANGUAGE plpgsql SECURITY DEFINER
16
- SET search_path TO 'public'
17
- AS $$
18
- DECLARE
19
- admin_flag_set boolean := false;
20
- user_role public.user_role;
21
- v_full_name text;
22
- v_avatar_url text;
23
- BEGIN
24
- INSERT INTO public.site_settings (key, value)
25
- VALUES ('is_admin_created', 'false'::jsonb)
26
- ON CONFLICT (key) DO NOTHING;
27
-
28
- SELECT COALESCE(value::jsonb::boolean, false)
29
- INTO admin_flag_set
30
- FROM public.site_settings
31
- WHERE key = 'is_admin_created'
32
- FOR UPDATE;
33
-
34
- IF admin_flag_set = false THEN
35
- user_role := 'ADMIN'::public.user_role;
36
-
37
- UPDATE public.site_settings
38
- SET value = 'true'::jsonb
39
- WHERE key = 'is_admin_created';
40
- ELSE
41
- user_role := 'USER'::public.user_role;
42
- END IF;
43
-
44
- v_full_name := NEW.raw_user_meta_data->>'full_name';
45
- v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';
46
-
47
- INSERT INTO public.profiles (
48
- id,
49
- role,
50
- full_name,
51
- avatar_url
52
- )
53
- VALUES (
54
- NEW.id,
55
- user_role,
56
- v_full_name,
57
- v_avatar_url
58
- )
59
- ON CONFLICT (id) DO UPDATE SET
60
- full_name = EXCLUDED.full_name,
61
- avatar_url = EXCLUDED.avatar_url;
62
-
63
- RETURN NEW;
64
- END;
65
- $$;
66
-
67
- -- 2. Drop the column now that nothing writes it.
68
- ALTER TABLE public.profiles DROP COLUMN IF EXISTS github_username;
69
-
70
- -- 3. Remove the GitHub-auth-only translation strings. connect_github is intentionally
71
- -- kept — it is still used by the self-update "Connect GitHub" button.
72
- DELETE FROM public.translations
73
- WHERE key IN (
74
- 'continue_with_github',
75
- 'github_username',
76
- 'github_username_help',
77
- 'github_link_failed',
78
- 'github_connected'
79
- );
1
+ -- Drop the github_username profile column and remove the GitHub-auth artifacts.
2
+ --
3
+ -- GitHub OAuth sign-in / account-linking was removed from the app: it cannot be
4
+ -- provisioned as part of a one-click deployment (each install needs its own GitHub
5
+ -- OAuth app + client secret + Supabase's manual-linking toggle, none of which can
6
+ -- ship in a migration). github_username was only ever populated from that OAuth
7
+ -- flow, so the column and its seed strings are now dead.
8
+ --
9
+ -- NOTE: the self-update "Connect GitHub" flow (lib/updates/*, ConnectGitHubButton)
10
+ -- is a separate feature and is intentionally left untouched — the connect_github
11
+ -- translation is kept for it.
12
+
13
+ -- 1. Rewrite handle_new_user() so new signups no longer read/populate github_username.
14
+ CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger
15
+ LANGUAGE plpgsql SECURITY DEFINER
16
+ SET search_path TO 'public'
17
+ AS $$
18
+ DECLARE
19
+ admin_flag_set boolean := false;
20
+ user_role public.user_role;
21
+ v_full_name text;
22
+ v_avatar_url text;
23
+ BEGIN
24
+ INSERT INTO public.site_settings (key, value)
25
+ VALUES ('is_admin_created', 'false'::jsonb)
26
+ ON CONFLICT (key) DO NOTHING;
27
+
28
+ SELECT COALESCE(value::jsonb::boolean, false)
29
+ INTO admin_flag_set
30
+ FROM public.site_settings
31
+ WHERE key = 'is_admin_created'
32
+ FOR UPDATE;
33
+
34
+ IF admin_flag_set = false THEN
35
+ user_role := 'ADMIN'::public.user_role;
36
+
37
+ UPDATE public.site_settings
38
+ SET value = 'true'::jsonb
39
+ WHERE key = 'is_admin_created';
40
+ ELSE
41
+ user_role := 'USER'::public.user_role;
42
+ END IF;
43
+
44
+ v_full_name := NEW.raw_user_meta_data->>'full_name';
45
+ v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';
46
+
47
+ INSERT INTO public.profiles (
48
+ id,
49
+ role,
50
+ full_name,
51
+ avatar_url
52
+ )
53
+ VALUES (
54
+ NEW.id,
55
+ user_role,
56
+ v_full_name,
57
+ v_avatar_url
58
+ )
59
+ ON CONFLICT (id) DO UPDATE SET
60
+ full_name = EXCLUDED.full_name,
61
+ avatar_url = EXCLUDED.avatar_url;
62
+
63
+ RETURN NEW;
64
+ END;
65
+ $$;
66
+
67
+ -- 2. Drop the column now that nothing writes it.
68
+ ALTER TABLE public.profiles DROP COLUMN IF EXISTS github_username;
69
+
70
+ -- 3. Remove the GitHub-auth-only translation strings. connect_github is intentionally
71
+ -- kept — it is still used by the self-update "Connect GitHub" button.
72
+ DELETE FROM public.translations
73
+ WHERE key IN (
74
+ 'continue_with_github',
75
+ 'github_username',
76
+ 'github_username_help',
77
+ 'github_link_failed',
78
+ 'github_connected'
79
+ );
@@ -1,21 +1,21 @@
1
- -- Restrict writes to the language-detection settings row to ADMIN only.
2
- --
3
- -- `site_settings.language_detection_settings` is a non-sensitive, anon-READABLE
4
- -- key (the request proxy reads it with the anon client to pick a visitor's first
5
- -- language). The CMS surfaces it under /cms/settings (ADMIN-only) and the server
6
- -- action guards with an ADMIN check, but the baseline write policies let ADMIN
7
- -- *or* WRITER write any non-sensitive key — so a WRITER could change site-wide
8
- -- detection directly via PostgREST. This migration adds the key to the ADMIN-only
9
- -- write group (INSERT/UPDATE/DELETE) to match the UI boundary, while leaving the
10
- -- SELECT policy untouched so the proxy's anon read keeps working.
11
- --
12
- -- Forward-only; recreates the three write policies idempotently.
13
-
14
- DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
15
- CREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
16
-
17
- DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
18
- CREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
19
-
20
- DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
21
- CREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
1
+ -- Restrict writes to the language-detection settings row to ADMIN only.
2
+ --
3
+ -- `site_settings.language_detection_settings` is a non-sensitive, anon-READABLE
4
+ -- key (the request proxy reads it with the anon client to pick a visitor's first
5
+ -- language). The CMS surfaces it under /cms/settings (ADMIN-only) and the server
6
+ -- action guards with an ADMIN check, but the baseline write policies let ADMIN
7
+ -- *or* WRITER write any non-sensitive key — so a WRITER could change site-wide
8
+ -- detection directly via PostgREST. This migration adds the key to the ADMIN-only
9
+ -- write group (INSERT/UPDATE/DELETE) to match the UI boundary, while leaving the
10
+ -- SELECT policy untouched so the proxy's anon read keeps working.
11
+ --
12
+ -- Forward-only; recreates the three write policies idempotently.
13
+
14
+ DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
15
+ CREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
16
+
17
+ DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
18
+ CREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
19
+
20
+ DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
21
+ CREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
@@ -0,0 +1,136 @@
1
+ -- 00000000000013_youtube_nocookie_embeds.sql
2
+ --
3
+ -- Lighthouse Best Practices scored 96 instead of 100: the `inspector-issues` audit
4
+ -- (binary, weight 1 of 27 -> 26/27 = 96) failed with a DevTools "Cookie" issue
5
+ -- attributed to www.youtube.com/embed/71MyfoL4YVM. The seeded home-page hero and the
6
+ -- seeded articles embed raw <iframe> markup pointing at www.youtube.com, whose player
7
+ -- writes cookies on load.
8
+ --
9
+ -- The real fix is the render-time click-to-play facade added in
10
+ -- apps/nextblock/components/media/YouTubeFacade.tsx (the nocookie host does NOT stop
11
+ -- the player's document.cookie writes -- only not loading the player does). This
12
+ -- migration is the data-hygiene half: it rewrites every stored embed to the
13
+ -- privacy-enhanced host so any surface that still renders a raw iframe (published-lib
14
+ -- renderers, CMS previews, exports, downstream installs on an older app build) is at
15
+ -- least cookie-reduced, and so the stored content matches what we ship.
16
+ --
17
+ -- It also fixes the bogus Permissions Policy feature in the seeded allow attribute:
18
+ -- 'accelerated-motion' is not a real feature (Chrome logs "Unrecognized feature");
19
+ -- the canonical YouTube embed uses 'accelerometer'.
20
+ --
21
+ -- Append-only: 00000000000003_baseline_seed.sql is applied everywhere and is NOT edited.
22
+ -- Scoped by CONTENT MATCH, never by block id -- ids drift on installs where the demo
23
+ -- content was re-created (see the convention documented in 00000000000006:11-13 and
24
+ -- used by 007/009). This is a pure host-substring swap, so it never clobbers a user's
25
+ -- edited copy, it also repairs author-pasted embeds, and it is naturally idempotent
26
+ -- (a second run matches zero rows) which matters because the nightly sandbox reset
27
+ -- replays the whole migration chain from a truncated history.
28
+ --
29
+ -- No dollar-quoting is needed here: neither search nor replacement literal contains a
30
+ -- single quote, and neither contains a JSON metacharacter, so the ::text round-trip
31
+ -- always re-parses as valid JSON. Query strings (?si=...) are preserved.
32
+ --
33
+ -- NOTE: keep the word "sandbox" OUT of this filename -- generate-sandbox-reset.ts
34
+ -- excludes any migration whose filename contains "sandbox".
35
+
36
+ -- 1. Live page/post block content (the 4 seeded rows + any author-added embed).
37
+ UPDATE public.blocks
38
+ SET content = replace(
39
+ replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
40
+ 'accelerated-motion', 'accelerometer'
41
+ )::jsonb
42
+ WHERE content IS NOT NULL
43
+ AND (content::text LIKE '%www.youtube.com/embed/%'
44
+ OR content::text LIKE '%accelerated-motion%');
45
+
46
+ -- 2. Live Draft Mode snapshots. Publishing a pre-existing draft would otherwise write
47
+ -- the cookie host straight back into public.blocks.
48
+ UPDATE public.content_drafts
49
+ SET blocks = replace(
50
+ replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
51
+ 'accelerated-motion', 'accelerometer'
52
+ )::jsonb
53
+ WHERE blocks::text LIKE '%www.youtube.com/embed/%'
54
+ OR blocks::text LIKE '%accelerated-motion%';
55
+
56
+ UPDATE public.content_drafts
57
+ SET meta = replace(
58
+ replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
59
+ 'accelerated-motion', 'accelerometer'
60
+ )::jsonb
61
+ WHERE meta::text LIKE '%www.youtube.com/embed/%'
62
+ OR meta::text LIKE '%accelerated-motion%';
63
+
64
+ UPDATE public.product_drafts
65
+ SET blocks = replace(
66
+ replace(blocks::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
67
+ 'accelerated-motion', 'accelerometer'
68
+ )::jsonb
69
+ WHERE blocks::text LIKE '%www.youtube.com/embed/%'
70
+ OR blocks::text LIKE '%accelerated-motion%';
71
+
72
+ -- product_drafts.meta carries short_description / description_json.
73
+ UPDATE public.product_drafts
74
+ SET meta = replace(
75
+ replace(meta::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
76
+ 'accelerated-motion', 'accelerometer'
77
+ )::jsonb
78
+ WHERE meta::text LIKE '%www.youtube.com/embed/%'
79
+ OR meta::text LIKE '%accelerated-motion%';
80
+
81
+ -- 3. Revision history. apps/nextblock/app/cms/revisions/service.ts replays a snapshot
82
+ -- verbatim, so one "Restore version" click would otherwise reintroduce the issue.
83
+ -- URL-only rewrite; nothing else about the historical snapshot is touched.
84
+ UPDATE public.page_revisions
85
+ SET content = replace(
86
+ replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
87
+ 'accelerated-motion', 'accelerometer'
88
+ )::jsonb
89
+ WHERE content::text LIKE '%www.youtube.com/embed/%'
90
+ OR content::text LIKE '%accelerated-motion%';
91
+
92
+ UPDATE public.post_revisions
93
+ SET content = replace(
94
+ replace(content::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
95
+ 'accelerated-motion', 'accelerometer'
96
+ )::jsonb
97
+ WHERE content::text LIKE '%www.youtube.com/embed/%'
98
+ OR content::text LIKE '%accelerated-motion%';
99
+
100
+ -- 4. Product rich text rendered on public /product/* routes.
101
+ UPDATE public.products
102
+ SET short_description = replace(
103
+ replace(short_description, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
104
+ 'accelerated-motion', 'accelerometer'
105
+ )
106
+ WHERE short_description IS NOT NULL
107
+ AND (short_description LIKE '%www.youtube.com/embed/%'
108
+ OR short_description LIKE '%accelerated-motion%');
109
+
110
+ UPDATE public.products
111
+ SET description_json = replace(
112
+ replace(description_json::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
113
+ 'accelerated-motion', 'accelerometer'
114
+ )::jsonb
115
+ WHERE description_json IS NOT NULL
116
+ AND (description_json::text LIKE '%www.youtube.com/embed/%'
117
+ OR description_json::text LIKE '%accelerated-motion%');
118
+
119
+ -- 5. Custom block definitions (rich-text defaults / emptyFallback markup rendered by
120
+ -- DynamicLayoutEngine). Host-only swap, so the is_valid_custom_block_* CHECK
121
+ -- constraints still hold. No-op on a fresh install.
122
+ UPDATE public.custom_block_definitions
123
+ SET fields = replace(
124
+ replace(fields::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
125
+ 'accelerated-motion', 'accelerometer'
126
+ )::jsonb
127
+ WHERE fields::text LIKE '%www.youtube.com/embed/%'
128
+ OR fields::text LIKE '%accelerated-motion%';
129
+
130
+ UPDATE public.custom_block_definitions
131
+ SET layout_schema = replace(
132
+ replace(layout_schema::text, 'www.youtube.com/embed/', 'www.youtube-nocookie.com/embed/'),
133
+ 'accelerated-motion', 'accelerometer'
134
+ )::jsonb
135
+ WHERE layout_schema::text LIKE '%www.youtube.com/embed/%'
136
+ OR layout_schema::text LIKE '%accelerated-motion%';