Files
flowdeck/images/Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html
T
bruno f04003f060
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
Add guide for cloning Notion databases and tasks
Also add screenshots of Notion trash menu and library section.
2026-07-09 16:40:20 -04:00

1522 lines
83 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Guide pour développer un clone de Notion : Bases de données &amp; Tâches</title>
<meta name="description" content="Difficulté : Avancé
Temps estimé : 3 à 6 mois pour une version complète frontend + backend
Résumé exécutif
Ce guide décortique chaque mécanisme visuel et fo">
<meta property="og:title" content="Guide pour développer un clone de Notion : Bases de données &amp; Tâches">
<meta property="og:description" content="Difficulté : Avancé
Temps estimé : 3 à 6 mois pour une version complète frontend + backend
Résumé exécutif
Ce guide décortique chaque mécanisme visuel et fo">
<meta property="og:type" content="article">
<meta property="og:image" content="https://images.ctfassets.net/spoqsaf9291f/7j6ATbu2ln8ItCgNQYyg7o/94b8cd7f8671bde645fb445e4a22c839/full-page-vs-inline-databases.png">
<meta name="theme-color" content="#b8543a" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#131214" media="(prefers-color-scheme: dark)">
<link rel="icon" href="data:image/svg+xml,&lt;svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'&gt;&lt;text y='75' font-size='75'&gt;O&lt;/text&gt;&lt;/svg&gt;">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--font-display: 'Charter', 'Iowan Old Style', Georgia, serif;
--font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--bg: #fbf9f4;
--bg-surface: #ffffff;
--bg-surface-alt: #f1ede4;
--border: rgba(0,0,0,0.08);
--border-strong: rgba(0,0,0,0.16);
--text: #1a1817;
--text-dim: #5a5651;
--text-muted: #8a8580;
--accent: #b8543a;
--accent-light: #d97a5e;
--accent-bg: rgba(184,84,58,0.06);
--gold: #c9952e;
--gold-bg: rgba(201,149,46,0.09);
--aurora-a: rgba(184,84,58,0.10);
--aurora-b: rgba(201,149,46,0.08);
--aurora-c: rgba(64,98,128,0.07);
--radius: 12px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.05);
--shadow-md: 0 4px 24px rgba(0,0,0,0.07);
--max-w: 760px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #131214; --bg-surface: #1c1a1e; --bg-surface-alt: #25232a;
--border: rgba(255,255,255,0.07); --border-strong: rgba(255,255,255,0.16);
--text: #ece8e2; --text-dim: #a8a39c; --text-muted: #6f6b66;
--accent: #e88f73; --accent-light: #f4ad95; --accent-bg: rgba(232,143,115,0.09);
--gold: #e8c05a; --gold-bg: rgba(232,192,90,0.09);
--aurora-a: rgba(232,143,115,0.13);
--aurora-b: rgba(232,192,90,0.09);
--aurora-c: rgba(125,180,224,0.10);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4); --shadow-md: 0 4px 28px rgba(0,0,0,0.55);
}
}
html {
scroll-behavior: smooth;
/* Give smooth-scroll some breathing room so anchors land below the
fixed toolbar instead of being shoved straight under it. */
scroll-padding-top: 4rem;
}
body {
font-family: var(--font-body);
background: var(--bg);
color: var(--text);
line-height: 1.75;
font-size: 17px;
font-feature-settings: 'ss01', 'cv11';
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
position: relative;
min-height: 100vh;
}
/* ── Aurora background ─────────────────────────────────
Slowly-drifting layered blobs in the accent palette. Sits behind
the content, fixed to the viewport so scrolling doesn't reset the
composition. Subtle grain on top stops it reading as 'flat CSS'. */
body::before {
content: '';
position: fixed;
inset: -20vh -20vw;
z-index: -2;
background:
radial-gradient(40vw 50vh at 18% 22%, var(--aurora-a) 0%, transparent 60%),
radial-gradient(45vw 55vh at 82% 12%, var(--aurora-b) 0%, transparent 65%),
radial-gradient(55vw 60vh at 50% 88%, var(--aurora-c) 0%, transparent 70%);
filter: blur(20px);
animation: aurora-drift 28s ease-in-out infinite alternate;
pointer-events: none;
}
body::after {
content: '';
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
/* Subtle film-grain — SVG turbulence baked to a data-URL. */
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
opacity: 0.045;
mix-blend-mode: overlay;
}
@keyframes aurora-drift {
0% { transform: translate3d(0,0,0) scale(1); }
50% { transform: translate3d(2vw,-1vh,0) scale(1.04); }
100% { transform: translate3d(-1vw,1.5vh,0) scale(1.02); }
}
@media (prefers-reduced-motion: reduce) {
body::before { animation: none; }
}
/* ── Toolbar (top-right) ──────────────────────────── */
.toolbar {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 100;
display: flex;
gap: 0.4rem;
opacity: 0.7;
transition: opacity 0.2s;
}
.toolbar:hover { opacity: 1; }
.toolbar button {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 14px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--bg-surface);
color: var(--text);
font-family: inherit;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: background 0.15s;
position: relative;
}
.toolbar button:hover { background: var(--bg-surface-alt); }
.toolbar button svg { width: 14px; height: 14px; flex-shrink: 0; }
.toolbar .toast {
position: absolute;
top: calc(100% + 6px);
right: 0;
background: var(--text);
color: var(--bg);
padding: 4px 10px;
border-radius: 6px;
font-size: 0.72rem;
white-space: nowrap;
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
}
.toolbar .toast.show { opacity: 1; }
.dropdown { position: relative; }
.dropdown-menu {
display: none;
position: absolute;
top: calc(100% + 4px);
right: 0;
background: var(--bg-surface);
border: 1px solid var(--border-strong);
border-radius: 8px;
box-shadow: var(--shadow-md);
overflow: hidden;
min-width: 140px;
}
.dropdown-menu.open { display: block; }
.dropdown-menu button {
display: block;
width: 100%;
padding: 8px 14px;
border: none;
background: none;
color: var(--text);
font-family: inherit;
font-size: 0.8rem;
text-align: left;
cursor: pointer;
}
.dropdown-menu button:hover { background: var(--bg-surface-alt); }
/* ── Hero ──────────────────────────────────────────── */
.hero {
position: relative;
background: transparent;
color: var(--text);
padding: 5.5rem 2rem 2.5rem;
text-align: center;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 70% 60% at 50% 40%, color-mix(in srgb, var(--accent) 10%, transparent) 0%, transparent 70%);
pointer-events: none;
}
/* A hair-thin gradient hairline divider under the hero to anchor it
without putting it on a heavy boxed background. */
.hero::after {
content: '';
position: absolute;
left: 50%; bottom: 0;
width: min(60%, 320px);
height: 1px;
transform: translateX(-50%);
background: linear-gradient(90deg, transparent, var(--border-strong), transparent);
}
.hero-label {
position: relative;
text-transform: uppercase;
letter-spacing: 0.28em;
font-size: 0.68rem;
font-weight: 600;
color: var(--accent);
opacity: 0.85;
margin-bottom: 1.4rem;
font-family: var(--font-body);
}
.hero h1 {
position: relative;
font-family: var(--font-display);
font-size: clamp(2rem, 4.5vw, 3rem);
font-weight: 600;
font-variation-settings: 'opsz' 120, 'SOFT' 50;
line-height: 1.15;
max-width: 720px;
margin: 0 auto;
letter-spacing: -0.02em;
color: var(--text);
}
/* ── Hero image ───────────────────────────────────── */
.hero-image {
max-width: var(--max-w);
margin: -2rem auto 0;
position: relative;
z-index: 1;
padding: 0 2rem;
}
.hero-image img {
width: 100%;
max-height: 360px;
object-fit: cover;
border-radius: var(--radius);
box-shadow: var(--shadow-md);
display: block;
}
/* ── Section images ───────────────────────────────── */
.section-image {
margin: 1.5rem 0;
position: relative;
}
.section-image img {
width: 100%;
max-height: 300px;
object-fit: cover;
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
display: block;
}
/* ── Per-image hide button ────────────────────────────
A small X that appears on hover (or always on touch) so users can
remove irrelevant OG images. Click POSTs the URL to the backend so
the next render skips it. */
.img-hide-btn {
position: absolute;
top: 10px; right: 10px;
width: 28px; height: 28px;
display: inline-flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.55);
color: #fff;
border: none;
border-radius: 50%;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease, transform 0.05s ease;
z-index: 2;
padding: 0;
}
.hero-image .img-hide-btn { top: 14px; right: 2.5rem; }
/* Reroll sits just to the left of the hide button. */
.img-reroll-btn {
position: absolute;
top: 10px; right: 46px;
width: 28px; height: 28px;
display: inline-flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.55);
color: #fff;
border: none;
border-radius: 50%;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease, transform 0.05s ease;
z-index: 2;
padding: 0;
}
.hero-image .img-reroll-btn { top: 14px; right: calc(2.5rem + 36px); }
.section-image:hover .img-hide-btn,
.section-image:hover .img-reroll-btn,
.hero-image:hover .img-hide-btn,
.hero-image:hover .img-reroll-btn { opacity: 1; }
.img-hide-btn:hover { background: var(--accent); }
.img-reroll-btn:hover { background: var(--accent); }
.img-hide-btn:active,
.img-reroll-btn:active { transform: scale(0.92); }
.img-reroll-btn.spinning svg { animation: img-reroll-spin 0.6s linear infinite; }
.img-reroll-btn:disabled { display: none; }
@keyframes img-reroll-spin { to { transform: rotate(360deg); } }
@media (hover: none) {
/* Touch devices have no hover — show the buttons at low opacity always. */
.img-hide-btn, .img-reroll-btn { opacity: 0.7; }
}
.section-image.fading,
.hero-image.fading {
opacity: 0;
transform: scale(0.96);
transition: opacity 0.25s ease, transform 0.25s ease;
}
/* ── Stats bar ─────────────────────────────────────── */
.stats-bar {
display: flex;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
padding: 0.9rem 2rem;
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
font-size: 0.82rem;
color: var(--text-dim);
}
.stat { display: flex; align-items: center; gap: 0.35rem; }
.stat-value { font-weight: 600; color: var(--text); }
/* ── Layout ────────────────────────────────────────── */
.layout {
display: grid;
grid-template-columns: 200px 1fr;
max-width: calc(var(--max-w) + 260px);
margin: 0 auto;
}
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.toc-sidebar { display: none; }
}
/* ── TOC sidebar ───────────────────────────────────── */
.toc-sidebar {
position: sticky; top: 0; height: 100vh; overflow-y: auto;
padding: 3.2rem 0.8rem 2rem 1.4rem;
border-right: 1px solid var(--border);
font-size: 0.78rem;
}
.toc-sidebar nav { position: relative; }
.toc-sidebar nav a {
position: relative;
display: block;
color: var(--text-dim);
text-decoration: none;
padding: 0.42rem 0.7rem 0.42rem 0.85rem;
margin: 1px 0;
border-radius: 6px;
line-height: 1.4;
letter-spacing: -0.005em;
transition: color 0.18s ease, background 0.18s ease, padding-left 0.18s ease;
}
/* Sliding accent indicator on the left edge of each TOC link */
.toc-sidebar nav a::before {
content: '';
position: absolute;
left: 0; top: 50%;
width: 2px; height: 0;
background: var(--accent);
transform: translateY(-50%);
border-radius: 1px;
transition: height 0.18s ease, opacity 0.18s ease;
opacity: 0;
}
.toc-sidebar nav a:hover {
color: var(--text);
background: var(--accent-bg);
padding-left: 1rem;
}
.toc-sidebar nav a:hover::before {
height: 60%;
opacity: 1;
}
.toc-sidebar nav a.active {
color: var(--accent);
font-weight: 600;
background: var(--accent-bg);
}
.toc-sidebar nav a.active::before {
height: 80%;
opacity: 1;
}
.toc-sidebar nav a.depth-3 {
padding-left: 1.3rem;
font-size: 0.72rem;
color: var(--text-muted);
}
.toc-sidebar nav a.depth-3:hover { padding-left: 1.45rem; }
/* ── Content ───────────────────────────────────────── */
.content { max-width: var(--max-w); padding: 3rem 2.5rem 4rem; }
/* Display headings — Fraunces optical-size driven so they get more
contrast and personality at the larger end. */
.content h2 {
font-family: var(--font-display);
font-size: clamp(1.55rem, 2.4vw, 1.85rem);
font-weight: 600;
font-variation-settings: 'opsz' 96, 'SOFT' 50;
margin: 3rem 0 1rem;
padding-bottom: 0.55rem;
border-bottom: 1px solid transparent;
border-image: linear-gradient(90deg, var(--accent) 0%, transparent 65%) 1;
letter-spacing: -0.022em;
line-height: 1.2;
color: var(--text);
}
.content h2:first-child { margin-top: 0; }
.content h3 {
font-family: var(--font-display);
font-size: 1.22rem;
font-weight: 600;
font-variation-settings: 'opsz' 32;
margin: 2.2rem 0 0.6rem;
letter-spacing: -0.015em;
color: var(--text);
}
.content h4 {
font-family: var(--font-body);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--text-dim);
margin: 1.6rem 0 0.5rem;
}
.content p { margin-bottom: 1.1rem; hanging-punctuation: first last; }
/* Drop cap on the very first paragraph of the body — old-school editorial
touch that anchors the reader. */
.content > p:first-of-type::first-letter,
.content > h2:first-child + p::first-letter {
font-family: var(--font-display);
font-weight: 700;
font-variation-settings: 'opsz' 144;
font-size: 3.6em;
line-height: 0.85;
float: left;
margin: 0.15em 0.12em 0 -0.04em;
color: var(--accent);
}
.content a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent);
text-decoration-thickness: 1.5px;
text-underline-offset: 3px;
transition: text-decoration-color 0.15s, color 0.15s;
}
.content a:hover {
text-decoration-color: var(--accent);
color: var(--accent-light);
}
.content ul, .content ol { margin: 0 0 1.1rem 1.6rem; }
.content li { margin-bottom: 0.4rem; }
.content li::marker { color: var(--accent); }
.content li > ul, .content li > ol { margin-top: 0.4rem; margin-bottom: 0; }
.content blockquote {
position: relative;
border-left: 3px solid var(--gold);
background: var(--gold-bg);
padding: 1.1rem 1.4rem 1.1rem 2.6rem;
margin: 1.5rem 0;
border-radius: 0 var(--radius) var(--radius) 0;
color: var(--text);
font-family: var(--font-display);
font-style: italic;
font-size: 1.05rem;
line-height: 1.55;
}
.content blockquote::before {
content: '\201C';
position: absolute;
left: 0.5rem; top: 0.3rem;
font-family: var(--font-display);
font-size: 3rem;
font-style: normal;
color: var(--gold);
opacity: 0.5;
line-height: 1;
}
.content hr { border: none; height: 1px; background: linear-gradient(90deg, transparent, var(--border-strong), transparent); margin: 2rem 0; }
.content code { font-family: var(--font-mono); font-size: 0.86em; background: var(--bg-surface-alt); padding: 0.15em 0.4em; border-radius: 4px; }
.content pre { background: var(--bg-surface-alt); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem 1.5rem; overflow-x: auto; margin: 1.25rem 0; font-size: 0.86rem; line-height: 1.6; }
.content pre code { background: none; padding: 0; }
.content table { width: 100%; border-collapse: collapse; margin: 1.25rem 0; font-size: 0.9rem; border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm); }
.content th { text-align: left; padding: 0.7rem 1rem; background: var(--accent-bg); font-weight: 600; border-bottom: 2px solid var(--border-strong); }
.content td { padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.content tr:last-child td { border-bottom: none; }
.content tr:hover td { background: var(--accent-bg); }
/* ── Sources (collapsible list) ───────────────────── */
.sources-panel { margin-top: 3rem; border-top: 2px solid var(--border); padding-top: 1.5rem; }
.sources-panel details { margin: 0; }
.sources-panel summary {
display: flex; align-items: center; gap: 0.5rem;
cursor: pointer; font-size: 1rem; font-weight: 600;
color: var(--text); padding: 0.5rem 0; list-style: none;
user-select: none;
}
.sources-panel summary::-webkit-details-marker { display: none; }
.sources-panel summary::before {
content: '\25B6'; font-size: 0.65em; color: var(--text-muted);
transition: transform 0.2s;
}
.sources-panel details[open] summary::before { transform: rotate(90deg); }
.sources-list { padding: 0.5rem 0 0 0.25rem; }
.sources-list a {
display: flex; align-items: baseline; gap: 0.5rem;
padding: 0.35rem 0; font-size: 0.85rem;
color: var(--text); text-decoration: none;
transition: color 0.15s;
}
.sources-list a:hover { color: var(--accent); }
.sources-list .snum {
color: var(--text-muted); font-size: 0.75rem;
min-width: 1.5rem; text-align: right; flex-shrink: 0;
}
.sources-list .sdomain {
color: var(--text-muted); font-size: 0.75rem;
margin-left: auto; flex-shrink: 0;
}
/* ── Chat-about CTA ────────────────────────────────── */
.chat-cta {
margin: 3rem 0 1rem; padding: 1.5rem;
text-align: center;
border: 1px solid var(--border); border-radius: 12px;
background: var(--bg-surface);
}
.chat-cta-btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 18px; font-size: 0.95rem; font-weight: 600;
background: var(--accent); color: #fff;
border: none; border-radius: 8px; cursor: pointer;
font-family: inherit;
transition: filter 0.15s, transform 0.05s;
}
.chat-cta-btn:hover:not(:disabled) { filter: brightness(1.1); }
.chat-cta-btn:active:not(:disabled) { transform: translateY(1px); }
.chat-cta-btn:disabled { opacity: 0.6; cursor: progress; }
.chat-cta-hint {
margin-top: 8px; font-size: 0.8rem; color: var(--text-muted);
}
/* ── Footer ────────────────────────────────────────── */
.report-footer {
text-align: center; padding: 2rem; font-size: 0.75rem;
color: var(--text-muted); border-top: 1px solid var(--border); margin-top: 2rem;
}
/* ── Animations ────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
.content h2, .content h3, .content p, .content ul, .content ol,
.content blockquote, .content table, .content pre, .section-image {
animation: fadeUp 0.4s ease both;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
}
/* ── Print ─────────────────────────────────────────── */
@media print {
.toc-sidebar, .toolbar { display: none !important; }
.layout { grid-template-columns: 1fr; }
.hero { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
/* ── Category palettes ───────────────────────────────────
Override the accent + aurora vars per category so each report
type has a distinct visual identity. */
body.category-product {
--accent: #2a8a8c;
--accent-light: #4ab0b2;
--accent-bg: rgba(42,138,140,0.07);
--aurora-a: rgba(42,138,140,0.11);
--aurora-b: rgba(201,149,46,0.06);
--aurora-c: rgba(64,98,128,0.06);
}
body.category-comparison {
--accent: #7a4cb8;
--accent-light: #9d76d0;
--accent-bg: rgba(122,76,184,0.07);
--aurora-a: rgba(122,76,184,0.11);
--aurora-b: rgba(184,84,58,0.05);
--aurora-c: rgba(64,98,128,0.07);
}
body.category-howto {
--accent: #3d8a3d;
--accent-light: #62b162;
--accent-bg: rgba(61,138,61,0.07);
--aurora-a: rgba(61,138,61,0.11);
--aurora-b: rgba(201,149,46,0.07);
--aurora-c: rgba(42,138,140,0.05);
}
body.category-landscape {
--accent: #b88a2e;
--accent-light: #d4a955;
--accent-bg: rgba(184,138,46,0.08);
--aurora-a: rgba(184,138,46,0.13);
--aurora-b: rgba(184,84,58,0.06);
--aurora-c: rgba(122,76,184,0.05);
}
@media (prefers-color-scheme: dark) {
body.category-product {
--accent: #5cc8cb; --accent-light: #8fdde0;
--accent-bg: rgba(92,200,203,0.10);
--aurora-a: rgba(92,200,203,0.13);
--aurora-b: rgba(232,192,90,0.07);
--aurora-c: rgba(125,180,224,0.08);
}
body.category-comparison {
--accent: #b896e8; --accent-light: #d0b8f0;
--accent-bg: rgba(184,150,232,0.10);
--aurora-a: rgba(184,150,232,0.13);
--aurora-b: rgba(232,143,115,0.06);
--aurora-c: rgba(125,180,224,0.08);
}
body.category-howto {
--accent: #82c882; --accent-light: #a8dba8;
--accent-bg: rgba(130,200,130,0.09);
--aurora-a: rgba(130,200,130,0.12);
--aurora-b: rgba(232,192,90,0.07);
--aurora-c: rgba(92,200,203,0.07);
}
body.category-landscape {
--accent: #e6c069; --accent-light: #f0d390;
--accent-bg: rgba(230,192,105,0.10);
--aurora-a: rgba(230,192,105,0.15);
--aurora-b: rgba(232,143,115,0.07);
--aurora-c: rgba(184,150,232,0.06);
}
}
/* ── Per-category font pairings ───────────────────────
Body font shifts between serif (long-form categories) and sans
(practical/data categories) so each report reads as a different
publication, not just a re-tinted version of the same template. */
/* Long-form: literary serif for both display and body */
body:not([class*="category-"]),
body.category-landscape {
--font-body: 'Source Serif 4', 'Iowan Old Style', Georgia, serif;
}
/* Comparison: analytical serif display + clean sans body */
body.category-comparison {
--font-display: 'Playfair Display', Georgia, serif;
--font-body: 'Inter', system-ui, sans-serif;
}
/* How-to: friendly geometric sans, top to bottom */
body.category-howto {
--font-display: 'Manrope', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
}
/* Product: techy/engineery — IBM Plex Sans display + Inter body */
body.category-product {
--font-display: 'IBM Plex Sans', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
}
/* Source Serif sits visually larger than Inter at the same px — pull it
back one notch for the categories that use it as body so line length
and rhythm stay comparable across categories. */
body:not([class*="category-"]) body, /* no-op selector, kept for clarity */
body.category-landscape { font-size: 16.5px; }
/* Drop cap looks bad on geometric sans — kill it for those categories */
body.category-product .content > p:first-of-type::first-letter,
body.category-howto .content > p:first-of-type::first-letter,
body.category-comparison .content > p:first-of-type::first-letter,
body.category-product .content > h2:first-child + p::first-letter,
body.category-howto .content > h2:first-child + p::first-letter,
body.category-comparison .content > h2:first-child + p::first-letter {
font-size: 1em; float: none; margin: 0; color: inherit;
font-family: inherit; font-weight: inherit;
}
/* ── Per-category background effects ───────────────
Each category overrides body::before so the page reads as a
distinctly-textured surface. Aurora stays the default. */
/* Product → blueprint grid that slowly pans */
body.category-product::before {
background:
linear-gradient(to right, var(--aurora-a) 1px, transparent 1px),
linear-gradient(to bottom, var(--aurora-a) 1px, transparent 1px),
radial-gradient(70vw 60vh at 50% 50%, var(--aurora-a) 0%, transparent 75%);
background-size: 56px 56px, 56px 56px, 100% 100%;
filter: none;
animation: cat-grid-pan 60s linear infinite;
}
@keyframes cat-grid-pan {
to { background-position: 56px 56px, 56px 56px, 0 0; }
}
/* Comparison → dot grid + slow opacity pulse */
body.category-comparison::before {
background:
radial-gradient(circle, var(--aurora-a) 1.4px, transparent 1.8px),
radial-gradient(60vw 55vh at 25% 25%, var(--aurora-b) 0%, transparent 65%),
radial-gradient(60vw 55vh at 75% 75%, var(--aurora-c) 0%, transparent 65%);
background-size: 26px 26px, 100% 100%, 100% 100%;
filter: none;
animation: cat-dot-pulse 14s ease-in-out infinite alternate;
}
@keyframes cat-dot-pulse {
from { opacity: 0.65; }
to { opacity: 1; }
}
/* How-to → flat surface with a very subtle vignette. Drop the flow-lines
pattern — it competes visually with the step number rails on the
right-hand side of each H2. The reading should feel like an O'Reilly
procedure: clean, scannable, no decoration in the way. */
body.category-howto::before {
background:
radial-gradient(70vw 70vh at 50% 0%, var(--aurora-a) 0%, transparent 60%),
radial-gradient(50vw 50vh at 50% 100%, var(--aurora-b) 0%, transparent 65%);
filter: blur(40px);
animation: none;
}
/* Landscape → horizontal horizon bands that slowly shift sideways */
body.category-landscape::before {
background:
linear-gradient(
180deg,
transparent 0%,
var(--aurora-a) 22%,
transparent 35%,
var(--aurora-b) 55%,
transparent 68%,
var(--aurora-c) 85%,
transparent 100%
);
background-size: 100% 200%;
filter: blur(40px);
animation: cat-horizon-drift 36s ease-in-out infinite alternate;
}
@keyframes cat-horizon-drift {
0% { background-position: 0 0; }
100% { background-position: 0 100%; }
}
@media (prefers-reduced-motion: reduce) {
body.category-product::before,
body.category-comparison::before,
body.category-howto::before,
body.category-landscape::before {
animation: none;
}
}
/* ─────────────────────────────────────────────────────
PER-CATEGORY STRUCTURAL TREATMENTS
Each category gets distinctive structural CSS so the page
reads as a different publication — not just retinted.
───────────────────────────────────────────────────── */
/* ── HOWTO: O'Reilly-style numbered procedure ─────── */
body.category-howto .content { counter-reset: howto-step; }
body.category-howto .content h2 {
counter-increment: howto-step;
display: flex; align-items: center; gap: 14px;
border-bottom: none;
padding-left: 0;
margin-top: 3.5rem;
}
body.category-howto .content h2::before {
content: counter(howto-step);
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
width: 40px; height: 40px;
border-radius: 12px;
background: var(--accent);
color: #fff;
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 700;
letter-spacing: 0;
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 30%, transparent);
}
/* Step body gets a colored left rail so you can scan "this is step 1's stuff" */
body.category-howto .content h2 ~ p,
body.category-howto .content h2 ~ ul,
body.category-howto .content h2 ~ ol,
body.category-howto .content h2 ~ pre,
body.category-howto .content h2 ~ blockquote {
border-left: 2px solid color-mix(in srgb, var(--accent) 25%, transparent);
padding-left: 1rem;
margin-left: 4px;
}
body.category-howto .content h2:has(+ *) ~ h2 ~ * { border-left: none; padding-left: 0; margin-left: 0; }
/* Terminal-style code blocks — green $ prompt, monospaced, dark surface */
body.category-howto .content pre {
background: #1a1a1e;
color: #d4e4d4;
border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
border-radius: 8px;
position: relative;
padding-left: 2.6rem;
}
body.category-howto .content pre::before {
content: '$';
position: absolute;
left: 1.1rem; top: 1.15rem;
color: var(--accent);
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.86rem;
opacity: 0.85;
}
body.category-howto .content pre code { color: inherit; }
/* ── LANDSCAPE: editorial briefing with H3 player cards ─ */
body.category-landscape .content h3 {
/* Each H3 in landscape = a "player" in the field — give it a card frame */
margin-top: 2.5rem;
padding: 14px 18px 4px;
border-left: 3px solid var(--accent);
background: color-mix(in srgb, var(--accent) 4%, transparent);
border-radius: 0 8px 8px 0;
font-family: var(--font-display);
font-size: 1.18rem;
}
body.category-landscape .content h3 + p {
margin-top: 0;
padding: 0 18px 14px;
background: color-mix(in srgb, var(--accent) 4%, transparent);
border-left: 3px solid var(--accent);
margin-left: 0;
border-radius: 0 0 8px 0;
}
/* Pull-quote treatment for any standalone blockquote */
body.category-landscape .content blockquote {
font-size: 1.2rem;
line-height: 1.5;
max-width: 90%;
margin: 2rem auto;
text-align: center;
border-left: none;
border-top: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
background: transparent;
border-radius: 0;
padding: 1.5rem 1rem;
font-style: italic;
}
body.category-landscape .content blockquote::before {
display: none;
}
/* ── COMPARISON: lab-report tables with winner badges ─ */
body.category-comparison .content {
font-feature-settings: 'tnum' on, 'ss01'; /* tabular numerals for tables */
}
body.category-comparison .content table {
font-size: 0.92rem;
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
}
body.category-comparison .content th {
background: color-mix(in srgb, var(--accent) 18%, var(--bg-surface));
color: var(--text);
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.72rem;
font-weight: 700;
}
body.category-comparison .content td:first-child {
font-weight: 600;
background: color-mix(in srgb, var(--accent) 6%, transparent);
}
/* The first H3 inside a comparison report often names the recommended pick */
body.category-comparison .content h3:first-of-type::after {
content: 'Pick';
display: inline-block;
margin-left: 10px;
padding: 2px 10px;
background: var(--accent);
color: #fff;
font-family: var(--font-body);
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
border-radius: 999px;
vertical-align: middle;
}
/* ── PRODUCT: spec-sheet cards for each H3 ─────────── */
body.category-product .content h3 {
/* Each product gets a spec-card frame — bordered, slight bg lift */
margin-top: 2.4rem;
padding: 16px 18px;
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
background: var(--bg-surface);
border-radius: 10px;
display: flex; align-items: baseline; gap: 10px;
font-family: var(--font-display);
letter-spacing: -0.01em;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
body.category-product .content h3::after {
/* small "spec" tag on each product heading */
content: 'SPEC';
margin-left: auto;
font-family: var(--font-body);
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.18em;
color: var(--accent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
padding: 3px 8px;
border-radius: 4px;
}
body.category-product .content h3 + p,
body.category-product .content h3 + ul,
body.category-product .content h3 + table {
margin-top: 0.8rem;
padding-left: 4px;
}
/* How-to category */
.category-howto .content h2 {
counter-increment:step-counter;
}
.category-howto .content h2::before {
content:counter(step-counter);
display:inline-flex; align-items:center; justify-content:center;
width:28px; height:28px; border-radius:50%;
background:var(--accent); color:#fff; font-size:0.8em; font-weight:700;
margin-right:10px; flex-shrink:0;
}
.category-howto .content { counter-reset:step-counter; }
.category-howto .content blockquote {
border-left:3px solid var(--accent); background:color-mix(in srgb, var(--accent) 8%, transparent);
padding:12px 16px; border-radius:0 6px 6px 0; margin:1em 0;
}
.category-howto .content blockquote strong:first-child {
display:inline-block; margin-bottom:4px; text-transform:uppercase;
font-size:0.82em; letter-spacing:0.5px;
}
.category-howto .content h2#quick-guide + ol,
.category-howto .content h2#quick-guide ~ ol:first-of-type {
background:color-mix(in srgb, var(--accent) 8%, transparent);
border:1px solid color-mix(in srgb, var(--accent) 20%, transparent);
border-radius:8px; padding:14px 14px 14px 32px; font-size:0.95em; line-height:1.8;
}
.category-howto .content h2#quick-guide {
counter-increment:none;
}
.category-howto .content h2#quick-guide::before {
content:'\26A1'; background:none; width:auto; height:auto; margin-right:6px;
}
</style>
</head>
<body class="category-howto">
<!-- Toolbar: Export + Restore hidden images -->
<div class="toolbar">
<div class="dropdown">
<button id="btn-export" title="Export">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Export ▾
</button>
<div class="dropdown-menu" id="export-menu">
<button id="btn-pdf">Save as PDF</button>
<button id="btn-html">Download HTML</button>
</div>
</div>
</div>
<div class="hero">
<div class="hero-label">Odysseus — Deep Research Report</div>
<h1>Guide pour développer un clone de Notion : Bases de données &amp; Tâches</h1>
</div>
<div class="hero-image" data-img-url="https://images.ctfassets.net/spoqsaf9291f/7j6ATbu2ln8ItCgNQYyg7o/94b8cd7f8671bde645fb445e4a22c839/full-page-vs-inline-databases.png"><img src="https://images.ctfassets.net/spoqsaf9291f/7j6ATbu2ln8ItCgNQYyg7o/94b8cd7f8671bde645fb445e4a22c839/full-page-vs-inline-databases.png" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"><button class="img-reroll-btn" type="button" title="Swap for another image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"></path></svg></button><button class="img-hide-btn" type="button" title="Hide image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></div>
<div class="stats-bar">
<div class="stat"><span class="stat-value">518.2s</span> Duration</div>
<div class="stat"><span class="stat-value">3</span> Rounds</div>
<div class="stat"><span class="stat-value">7</span> Queries</div>
<div class="stat"><span class="stat-value">25</span> URLs Analyzed</div>
<div class="stat"><span class="stat-value">deepseek-v4-pro</span> Model</div>
<div class="stat"><span class="stat-value">searxng</span> Search</div>
</div>
<div class="layout">
<aside class="toc-sidebar">
<nav>
<a href="#r-sum-ex-cutif" class="depth-3 active">Résumé exécutif</a>
<a href="#quick-guide-tapes-de-clonage" class="depth-2">Quick Guide (étapes de clonage)</a>
<a href="#pr-requis" class="depth-2">Prérequis</a>
<a href="#step-1-les-blocs-de-base-de-donn-es-cr-ation-inline-pleine-page" class="depth-2">Step 1: Les blocs de base de données (création, inline/pleine page)</a>
<a href="#step-2-le-syst-me-de-propri-t-s-et-le-mod-le-de-donn-es" class="depth-2">Step 2: Le système de propriétés et le modèle de données</a>
<a href="#propri-t-s-de-type-statut-et-t-ches" class="depth-3">Propriétés de type Statut et tâches</a>
<a href="#la-propri-t-date-interactive-et-les-rappels" class="depth-3">La propriété Date interactive et les rappels</a>
<a href="#gestion-des-propri-t-s" class="depth-3">Gestion des propriétés</a>
<a href="#step-3-le-moteur-de-vues-filtres-tris-groupements" class="depth-2">Step 3: Le moteur de vues (filtres, tris, groupements)</a>
<a href="#step-4-d-veloppement-des-vues-sp-cialis-es" class="depth-2">Step 4: Développement des vues spécialisées</a>
<a href="#vue-table" class="depth-3">Vue Table</a>
<a href="#vue-board-kanban" class="depth-3">Vue Board (kanban)</a>
<a href="#vue-gallery" class="depth-3">Vue Gallery</a>
<a href="#vue-list" class="depth-3">Vue List</a>
<a href="#step-5-layouts-des-pages-d-une-base" class="depth-2">Step 5: Layouts des pages d’une base</a>
<a href="#l-ments-du-layout" class="depth-3">Éléments du layout</a>
<a href="#structures-disponibles" class="depth-3">Structures disponibles</a>
<a href="#step-6-conversion-en-base-de-t-ches-sous-t-ches-d-pendances-sprints" class="depth-2">Step 6: Conversion en base de tâches – sous‑tâches, dépendances, sprints</a>
<a href="#sous-t-ches-sub-items" class="depth-3">Sous‑tâches (Sub‑items)</a>
<a href="#d-pendances" class="depth-3">Dépendances</a>
<a href="#sprints" class="depth-3">Sprints</a>
<a href="#step-7-automatisations-et-rappels-multicanaux" class="depth-2">Step 7: Automatisations et rappels multicanaux</a>
<a href="#automatisations-de-base-de-donn-es" class="depth-3">Automatisations de base de données</a>
<a href="#syst-me-de-rappels-int-gr-aux-dates" class="depth-3">Système de rappels intégré aux dates</a>
<a href="#step-8-interface-coh-rente-et-interactions-globales" class="depth-2">Step 8: Interface cohérente et interactions globales</a>
<a href="#erreurs-courantes" class="depth-2">Erreurs courantes</a>
</nav>
</aside>
<main class="content">
<p><strong>Difficulté :</strong> Avancé<br>
<strong>Temps estimé :</strong> 3 à 6 mois pour une version complète (frontend + backend)</p>
<h3 id="r-sum-ex-cutif">Résumé exécutif</h3>
<p>Ce guide décortique chaque mécanisme visuel et fonctionnel des bases de données Notion, ainsi que l’implémentation native des tâches (sous‑tâches, dépendances, sprints), pour permettre à un développeur de reproduire fidèlement ces briques. Basé sur l’analyse des pages d’aide officielles, il extrait les spécifications précises de l’interface utilisateur, du modèle de données et des interactions clavier/souris. Il couvre les commandes slash, les propriétés configurables (dont les dates avec rappels multicanaux), les vues Table, Board, Gallery et List, les layouts de pages, ainsi que le passage en mode « base de tâches » et les automatisations. Chaque section fournit des détails d’implémentation et des citations vers la documentation source.</p>
<hr>
<h2 id="quick-guide-tapes-de-clonage">Quick Guide (étapes de clonage)</h2>
<ol>
<li>Construire un éditeur de blocs avec support inline/pleine page et commandes slash.</li>
<li>Modéliser les propriétés : types variés, édition masse, cellule date interactive avec rappel.</li>
<li>Créer le moteur de vue : filtres ET/OU imbriqués, tris, groupements, propriétés visibles par vue.</li>
<li>Développer les vues Table, Board, Gallery et List avec leurs réglages spécifiques.</li>
<li>Implémenter les layouts de page (en‑tête épinglé, groupe de propriétés, panneau latéral, onglets).</li>
<li>Activer le mode tâche : auto‑relation pour sous‑tâches, dépendances via glisser de flèches, sprints.</li>
<li>Intégrer un système d’automatisations déclencheur‑condition‑action et des rappels multicanaux.</li>
<li>Appliquer partout glisser‑déposer, infobulles et conversion inline ↔ pleine page.</li>
</ol>
<hr>
<h2 id="pr-requis">Prérequis</h2>
<figure class="section-image" data-img-url="https://images.ctfassets.net/spoqsaf9291f/4MtyLDohhpUy0okjX9TEty/edebe0381af6149035a13266a66dd00b/databases-reimagined.png"><img src="https://images.ctfassets.net/spoqsaf9291f/4MtyLDohhpUy0okjX9TEty/edebe0381af6149035a13266a66dd00b/databases-reimagined.png" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"><button class="img-reroll-btn" type="button" title="Swap for another image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"></path></svg></button><button class="img-hide-btn" type="button" title="Hide image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></figure>
<p>Avant de commencer, vous devez maîtriser :
- Un framework frontend réactif (React, Vue, Svelte) avec gestion d’état complexe (stores, contextes).
- Le rendu temps réel de blocs (comme Slate, ProseMirror, ou un éditeur maison).
- Une API backend ou une base de données temps réel (Firestore, PostgreSQL avec websockets) pour le collaboratif.
- Les concepts de propriétés calculées (rollups, formules) et d’auto‑relations (liens bidirectionnels).</p>
<hr>
<h2 id="step-1-les-blocs-de-base-de-donn-es-cr-ation-inline-pleine-page">Step 1: Les blocs de base de données (création, inline/pleine page)</h2>
<p>Dans Notion, une base de données peut être créée de deux manières :
- <strong>Pleine page</strong> : <code>/page</code>, puis choix d’un type de vue (Table, Board, etc.) dans l’écran « New page » <a href="https://www.notion.com/help/guides/block-basics-build-the-foundation-for-your-teams-pages" rel="noopener noreferrer" target="_blank">Block basics</a>.
- <strong>Inline</strong> : via les commandes slash <code>/database</code>, <code>/table</code>, <code>/board</code>, <code>/list</code>, <code>/calendar</code>, <code>/gallery</code> ou <code>/timeline</code> insérées dans le corps d’une page existante.</p>
<blockquote>
<p><strong>Tip:</strong> Traitez chaque bloc base comme un nœud du document pouvant être converti à la volée. Un bouton <strong>⤢ (expand)</strong> apparaît au survol d’une base inline pour la transformer en page pleine, et inversement, l’option « Turn into inline » est disponible dans le menu d’une page pleine glissée dans une autre page <a href="https://www.notion.com/help/lists" rel="noopener noreferrer" target="_blank">List view</a>.</p>
</blockquote>
<p>Votre modèle de données doit donc contenir un type <code>block</code> avec une propriété <code>view_type</code> (table, board, etc.) et une propriété <code>inline</code> booléenne. La représentation visuelle bascule entre un composant embarqué (redimensionnable) et une route dédiée. Le menu «<code>...</code>» en haut à droite de la base donne accès à toutes les options globales : verrouillage (saisie autorisée mais structure figée), édition des propriétés, automatisations, activation des sous‑éléments, dépendances, sprints, et conversion en base de tâches <a href="https://www.notion.com/help/customize-your-database" rel="noopener noreferrer" target="_blank">Database settings</a>. L’interface doit donc afficher ce menu contextuel uniquement si l’utilisateur a les droits d’édition.</p>
<hr>
<h2 id="step-2-le-syst-me-de-propri-t-s-et-le-mod-le-de-donn-es">Step 2: Le système de propriétés et le modèle de données</h2>
<figure class="section-image" data-img-url="https://images.ctfassets.net/spoqsaf9291f/1JhmGEelQl3xaWh6KTvN1L/7f5e076fe8fa246f1739608d07060e86/Reference_visuals_Oct_22_Screenshot_1.png"><img src="https://images.ctfassets.net/spoqsaf9291f/1JhmGEelQl3xaWh6KTvN1L/7f5e076fe8fa246f1739608d07060e86/Reference_visuals_Oct_22_Screenshot_1.png" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"><button class="img-reroll-btn" type="button" title="Swap for another image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"></path></svg></button><button class="img-hide-btn" type="button" title="Hide image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></figure>
<p>Une base Notion est une collection d’items (pages) dotés de propriétés. L’aide officielle liste une vingtaine de types : Texte, Nombre, Sélection, Statut, Case à cocher, Date, Personne, Fichier &amp; média, URL, Email, Téléphone, Formule, Relation, Rollup, etc. <a href="https://www.notion.com/help/database-properties" rel="noopener noreferrer" target="_blank">Database properties</a>.</p>
<h3 id="propri-t-s-de-type-statut-et-t-ches">Propriétés de type Statut et tâches</h3>
<p>Le type <code>Statut</code> est central pour les tâches. Il affiche par défaut les étiquettes <em>To‑do / In Progress / Complete</em>, chacune associée à une couleur configurable. Contrairement à une simple case à cocher (Checkbox), le Statut permet de définir un flux de travail personnalisé (ex. « Backlog, À faire, En cours, Terminé »).</p>
<h3 id="la-propri-t-date-interactive-et-les-rappels">La propriété Date interactive et les rappels</h3>
<p>La cellule de type Date est un composant essentiel. En cliquant dessus, un mini‑calendrier s’ouvre et propose :
- La sélection d’une date avec ou sans heure (une case « Inclure l’heure » est cochée par défaut pour les types incluant l’heure).
- Une option <strong>Rappel</strong> avec des délais prédéfinis (« Le jour même », « 1 jour avant », « 2 jours avant », « Heure personnalisée ») <a href="https://www.notion.com/fr/help/reminders" rel="noopener noreferrer" target="_blank">Rappels Notion</a> <a href="https://www.notion.com/help/database-properties" rel="noopener noreferrer" target="_blank">Database properties</a>.</p>
<p>Quand un rappel est activé, une <strong>icône d’horloge rouge</strong> apparaît dans la cellule. Les notifications sont multicanaux :
- un badge rouge sur l’icône de la boîte de réception dans la barre latérale,
- une notification push desktop dans les 5 minutes,
- une notification push mobile,
- et un e‑mail si aucun appareil n’est connecté <a href="https://www.notion.com/fr/help/reminders" rel="noopener noreferrer" target="_blank">Rappels</a>.</p>
<blockquote>
<p><strong>Warning:</strong> Les tâches récurrentes ne sont pas supportées nativement ; les utilisateurs doivent les contourner via des formules de date ou des automatisations de duplication. Si vous clonez Notion, prévoyez éventuellement une récurrence en tant que fonctionnalité additionnelle.</p>
</blockquote>
<h3 id="gestion-des-propri-t-s">Gestion des propriétés</h3>
<p>Le bouton « Edit properties » du menu ouvre un gestionnaire où l’on peut rechercher, créer, éditer, dupliquer ou supprimer une propriété, ainsi que réorganiser leur ordre par glisser‑déposer. Chaque vue peut indépendamment masquer ou réorganiser les propriétés via un panneau de réglages <a href="https://www.notion.com/help/views-filters-and-sorts" rel="noopener noreferrer" target="_blank">Views, filters, sorts &amp; groups</a>. Cela signifie que vous devez stocker pour chaque vue une liste de propriétés visibles avec leur ordre, séparée de la définition globale des propriétés de la base.</p>
<hr>
<h2 id="step-3-le-moteur-de-vues-filtres-tris-groupements">Step 3: Le moteur de vues (filtres, tris, groupements)</h2>
<p>Une base peut posséder <strong>plusieurs vues</strong>, matérialisées par des onglets en haut, réorganisables par glisser‑déposer, avec icône et nom personnalisables. Un bouton <code>+</code> ajoute une nouvelle vue <a href="https://www.notion.com/help/views-filters-and-sorts" rel="noopener noreferrer" target="_blank">Views, filters, sorts &amp; groups</a>.</p>
<p>Chaque vue dispose d’un panneau de réglages accessible via <code>…</code> :
- <strong>Layout</strong> : choix du type (Table, Board, Timeline, Calendar, List, Gallery, Chart).
- <strong>Properties</strong> : activation/désactivation et réorganisation des propriétés via des poignées <code>⋮⋮</code>.
- <strong>Filters</strong> : support de filtres simples et <strong>avancés</strong>, ces derniers autorisant des groupes de conditions ET/OU jusqu’à <strong>3 niveaux d’imbrication</strong>. Implémentez un éditeur visuel d’arbres de conditions.
- <strong>Sorts</strong> : un ou plusieurs critères avec ordre ascendant/descendant.
- <strong>Groups</strong> : regroupement par une propriété (ex. Statut, Assigné) avec la possibilité de <strong>sous‑groupes</strong> (deux niveaux).
- <strong>Open pages in</strong> : détermine si l’ouverture se fait en « side peek », « center peek » ou « full page » pour cette vue.
- <strong>Freeze column</strong> (Table uniquement) : fige la première colonne.
- <strong>Search</strong> : barre de recherche textuelle toujours visible.</p>
<p>Les <strong>filtres rapides</strong> (Quick filters) s’affichent à droite des onglets ; ce sont des raccourcis contextuels comme « My tasks », « Assigned to me », modifiables sans ouvrir le panneau complet. Leur configuration est sauvegardée par vue <a href="https://www.notion.com/help/guides/databases-reimagined-whats-changed" rel="noopener noreferrer" target="_blank">Databases reimagined</a>. Stockez donc, pour chaque vue, un objet <code>quick_filters</code> contenant des identifiants de conditions prédéfinies.</p>
<hr>
<h2 id="step-4-d-veloppement-des-vues-sp-cialis-es">Step 4: Développement des vues spécialisées</h2>
<figure class="section-image" data-img-url="https://www.notion.comhttps://img.youtube.com/vi/-izAC1eour4/maxresdefault.jpg" style="display: none;"><img src="https://www.notion.comhttps://img.youtube.com/vi/-izAC1eour4/maxresdefault.jpg" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"><button class="img-reroll-btn" type="button" title="Swap for another image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"></path></svg></button><button class="img-hide-btn" type="button" title="Hide image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></figure>
<h3 id="vue-table">Vue Table</h3>
<p>C’est l’affichage par défaut. Les fonctionnalités à reproduire incluent :
- Redimensionnement des colonnes.
- Édition rapide en cliquant sur une cellule (le champ se transforme en input natif ou en composant de type propriété).
- Sélection multiple de lignes.
- Menu de la vue permettant de <strong>geler la première colonne</strong> (titre) et d’afficher des <strong>calculs en bas de certaines colonnes</strong> (somme, moyenne, médiane, etc.) selon le type de propriété.
- La colonne de titre (première colonne) contient le nom de l’item et s’ouvre sur la page de l’élément. Implémentez un clic sur la cellule titre avec le comportement d’ouverture paramétré (side peek, etc.).</p>
<h3 id="vue-board-kanban">Vue Board (kanban)</h3>
<p>Créez-la avec l’option <em>Add a view &gt; Board</em> ou <code>/board</code> <a href="https://www.notion.com/help/boards" rel="noopener noreferrer" target="_blank">Board view</a>. Elle est basée sur un groupement par propriété de type Sélection, Statut ou Personne. Chaque colonne correspond à une valeur de cette propriété. Les colonnes peuvent être masquées, mais les éléments restent accessibles via d’autres vues.</p>
<p>Les cartes sont hautement configurables :
- <strong>Prévisualisation</strong> : aucune, image de couverture, premier bloc de la page (utile pour des images intégrées), ou image issue d’une propriété Fichier &amp; média.
- <strong>Taille des cartes</strong> réglable.
- <strong>Affichage/masquage du titre</strong> de la carte.
- <strong>Propriétés visibles</strong> : sélection des propriétés à afficher directement sur la carte (ex. Assigné, Date d’échéance, Priorité). Le panneau de réglages de la vue Board permet de cocher ces propriétés.</p>
<p>Le <strong>glisser‑déposer</strong> d’une carte d’une colonne à l’autre modifie automatiquement la valeur du champ de groupement pour cet élément. C’est une interaction critique qui doit déclencher une mise à jour immédiate de la base de données (ou d’un état local).</p>
<p>Des <strong>calculs</strong> (nombre d’éléments, somme) peuvent être affichés en haut de chaque colonne.</p>
<h3 id="vue-gallery">Vue Gallery</h3>
<p>Elle ressemble à une grille de cartes personnalisables <a href="https://www.notion.com/help/galleries" rel="noopener noreferrer" target="_blank">Gallery view</a>. Reprenez les options de prévisualisation de la vue Board. Ajoutez :
- Un toggle <em>Fit image</em> qui adapte le recadrage et autorise un repositionnement au survol.
- Un réglage de <strong>taille de carte</strong> (<em>Card size</em>).
- La possibilité de <strong>masquer le nom</strong> de la carte pour un effet mood board.
- Les mêmes options de filtrage, tri et propriétés visibles que les autres vues.</p>
<h3 id="vue-list">Vue List</h3>
<p>La vue List est minimaliste : les éléments sont affichés verticalement, et les propriétés choisies apparaissent en ligne à droite du titre <a href="https://www.notion.com/help/lists" rel="noopener noreferrer" target="_blank">List view</a>. Dans le panneau de réglages, chaque propriété peut être activée/désactivée et réorganisée via des poignées <code>⋮⋮</code>. Au survol d’un élément, un bouton <code>⤢</code> permet d’ouvrir la page en mode plein écran.</p>
<blockquote>
<p><strong>Tip:</strong> La conversion inline ↔ pleine page est omniprésente : utilisez le même bouton <code>⤢</code> en survol d’une base inline, et « Turn into inline » dans le menu de la base pleine page. Cette fonctionnalité doit être un toggle simple qui change la propriété <code>inline</code> du bloc et charge le composant adéquat.</p>
</blockquote>
<hr>
<h2 id="step-5-layouts-des-pages-d-une-base">Step 5: Layouts des pages d’une base</h2>
<p>Chaque page d’une base possède une structure paramétrable depuis le menu « Customize page layout » <a href="https://www.notion.com/help/layouts" rel="noopener noreferrer" target="_blank">Layouts</a>. Cela détermine comment les propriétés et le contenu sont affichés lorsque l’utilisateur ouvre un item de la base.</p>
<h3 id="l-ments-du-layout">Éléments du layout</h3>
<ul>
<li><strong>En‑tête (Heading)</strong> : obligatoire, il montre le titre de la page et jusqu’à <strong>15 propriétés épinglées horizontalement</strong> (avec un défilement si nécessaire). Les autres propriétés sont reléguées dans le <em>Groupe de propriétés</em>.</li>
<li><strong>Groupe de propriétés</strong> : conteneur unique placé dans la zone principale qui liste les propriétés non épinglées, organisables en sections nommées, avec une recherche en temps réel et une option pour masquer les icônes des propriétés.</li>
<li><strong>Modules</strong> : des blocs personnalisés (comme des vues intégrées d’autres bases) peuvent être ajoutés dans la zone principale.</li>
<li><strong>Panneau latéral</strong> : collapsible, il peut héberger soit des propriétés individuelles, soit le Groupe de propriétés en mode compact.</li>
</ul>
<h3 id="structures-disponibles">Structures disponibles</h3>
<ul>
<li><strong>Simple</strong> : zone principale + panneau latéral.</li>
<li><strong>Tabbed</strong> : un onglet « Content » contenant les modules, et des onglets supplémentaires affichant des vues live d’autres bases (par exemple, un onglet « Réunions » intégrant une base liée).</li>
</ul>
<p>Les paramètres additionnels incluent l’affichage des commentaires en ligne (default ou minimal), l’affichage des discussions de page, et le passage en pleine largeur. Implémentez un système de template par base, avec une configuration JSON décrivant l’épinglage des propriétés, l’ordonnancement des modules et l’état du panneau latéral.</p>
<hr>
<h2 id="step-6-conversion-en-base-de-t-ches-sous-t-ches-d-pendances-sprints">Step 6: Conversion en base de tâches – sous‑tâches, dépendances, sprints</h2>
<figure class="section-image" data-img-url="https://images.ctfassets.net/spoqsaf9291f/tuPWSxsJZ4eprqMPVd7c5/7f01fe24939e5057a1dd03eb70351b74/creating-a-database.png"><img src="https://images.ctfassets.net/spoqsaf9291f/tuPWSxsJZ4eprqMPVd7c5/7f01fe24939e5057a1dd03eb70351b74/creating-a-database.png" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"><button class="img-reroll-btn" type="button" title="Swap for another image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"></path></svg></button><button class="img-hide-btn" type="button" title="Hide image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></figure>
<p>L’option <em>Convert to task database</em> (dans le menu <code>...</code> &gt; Customize database) enrichit une base existante avec trois extensions <a href="https://www.notion.com/help/customize-your-database" rel="noopener noreferrer" target="_blank">Database settings</a>. Elle est réversible. La conversion est purement sémantique : elle active des propriétés spéciales et des mécanismes d’affichage sans changer le stockage sous‑jacent.</p>
<h3 id="sous-t-ches-sub-items">Sous‑tâches (Sub‑items)</h3>
<p>L’activation crée automatiquement une <strong>relation auto‑référencée</strong> qui se scinde en deux champs miroirs : <em>Parent tasks</em> et <em>Sub‑items</em> <a href="https://www.notion.com/help/guides/tasks-manageable-steps-sub-tasks-dependencies" rel="noopener noreferrer" target="_blank">Sub‑tasks and dependencies</a>.</p>
<p><strong>Implémentation</strong> : vous ajoutez à la base une propriété de type Relation pointant vers la même base, avec un affichage bidirectionnel. En Table ou Board, le survol d’une ligne fait apparaître un menu permettant d’ajouter une sous‑tâche directement (création inline). Sur la page de l’item, les sous‑tâches peuvent apparaître soit comme une simple <em>pill</em> de relation, soit comme une <strong>section dédiée</strong> affichant des propriétés configurables (statut, assigné, date d’échéance) et proposant un champ de saisie pour créer de nouvelles sous‑tâches. Cette section est en réalité une vue filtrée de la même base, montrant tous les items dont la propriété <em>Parent task</em> pointe vers l’item courant.</p>
<h3 id="d-pendances">Dépendances</h3>
<p>Activez <em>Dependencies</em> dans le menu, ce qui ajoute une autre relation auto‑référencée nommée <em>Dependencies</em>. Dans la vue <strong>Timeline</strong> (Gantt), lorsque l’option est cochée, des poignées apparaissent sur les barres. L’utilisateur peut alors <strong>glisser une flèche</strong> d’une barre à l’autre pour créer un lien. Les flèches se redessinent automatiquement si les dates sont modifiées. Pour le cloner, il faut un canevas SVG/Canvas réactif capable de gérer ces interactions de glisser‑création de lien.</p>
<h3 id="sprints">Sprints</h3>
<p>Disponible uniquement après la conversion en base de tâches. Notion ajoute une propriété « sprint » (probablement de type Sélection) pour organiser les tâches par cycle. Sa gestion est identique à toute autre propriété de sélection.</p>
<hr>
<h2 id="step-7-automatisations-et-rappels-multicanaux">Step 7: Automatisations et rappels multicanaux</h2>
<h3 id="automatisations-de-base-de-donn-es">Automatisations de base de données</h3>
<p>Une icône <strong>éclair</strong> (<code>⚡</code>) dans le menu de la base ouvre le panneau des automatisations. Le modèle est <strong>déclencheur → condition → action</strong>, avec par exemple :
- Déclencheur : « Quand une propriété Date arrive dans 1 jour »
- Action : « Envoyer une notification à [membre] »</p>
<p>Les automatisations s’appliquent à l’échelle de la base entière, sans paramétrage par élément <a href="https://2sync.com/fr/blog/comment-definir-des-rappels-dans-notion" rel="noopener noreferrer" target="_blank">Automatisations</a> (source communautaire, mais confirmée par l’interface Notion). Pour votre clone, vous devez implémenter un moteur de règles évaluées côté serveur (ou via des crons), déclenchant des notifications, modifications de propriétés, ou envois de webhooks.</p>
<h3 id="syst-me-de-rappels-int-gr-aux-dates">Système de rappels intégré aux dates</h3>
<p>Nous l’avons vu à l’étape 2 : chaque cellule Date peut contenir un rappel. Les notifications générées empruntent le même pipeline multicanaux que les automatisations (badge rouge, push, email). Il vous faudra une file de jobs planifiés (par exemple avec un scheduler type Bull ou un service externe) qui vérifie les rappels arrivant à échéance et envoie les événements aux clients connectés.</p>
<blockquote>
<p><strong>Warning:</strong> Les notifications push desktop doivent respecter les contraintes des navigateurs ; prévoyez un service worker pour la version web, et des mécanismes natifs pour mobile. L’e‑mail de secours nécessite un service d’envoi transactionnel.</p>
</blockquote>
<hr>
<h2 id="step-8-interface-coh-rente-et-interactions-globales">Step 8: Interface cohérente et interactions globales</h2>
<figure class="section-image" data-img-url="https://www.notion.comhttps://img.youtube.com/vi/Nx114VWepoI/maxresdefault.jpg" style="display: none;"><img src="https://www.notion.comhttps://img.youtube.com/vi/Nx114VWepoI/maxresdefault.jpg" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"><button class="img-reroll-btn" type="button" title="Swap for another image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"></path></svg></button><button class="img-hide-btn" type="button" title="Hide image"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></figure>
<p>Au‑delà des fonctionnalités individuelles, Notion s’appuie sur des patterns d’interaction que vous devez reproduire pour une expérience fluide :
- <strong>Menus contextuels</strong> : ouverture d’un menu <code>...</code> ou clic droit pour les actions locales.
- <strong>Glisser‑déposer omniprésent</strong> : réorganisation des propriétés, des vues, des cartes, des colonnes, des liens de dépendance. Utilisez une bibliothèque comme <code>dnd-kit</code> ou <code>react-beautiful-dnd</code>.
- <strong>Verrouillage</strong> : l’état de verrouillage d’une base bloque toute modification de structure (vues, propriétés) mais autorise la saisie de données. Affichez un cadenas et désactivez les boutons d’édition correspondants.
- <strong>Conversion inline ↔ pleine page</strong> : le bouton <code>⤢</code> doit être présent partout (survol d’une base inline, survol d’un élément de liste, etc.). Son état doit être réversible.
- <strong>Recherche</strong> : barre permanente dans chaque vue, et recherche en temps réel dans le groupe de propriétés.
- <strong>Affichage des permissions</strong> : les options de modification ne sont offertes qu’aux utilisateurs ayant des droits d’édition ; les vues et propriétés se masquent ou s’affichent selon les droits de partage.</p>
<hr>
<h2 id="erreurs-courantes">Erreurs courantes</h2>
<ul>
<li><strong>Confondre « base de données » et « vue »</strong> : une base unique peut avoir plusieurs vues indépendantes. Ne pas stocker les filtres ou le type d’affichage au niveau de la base, mais bien au niveau de la vue.</li>
<li><strong>Négliger l’imbrication des filtres</strong> : les filtres avancés sur 3 niveaux ET/OU nécessitent un éditeur récursif. Une interface trop simpliste sera immédiatement frustrante.</li>
<li><strong>Oublier la synchronisation bidirectionnelle des relations</strong> : quand vous reliez A à B en tant que sous‑tâche, la propriété inversée <em>Parent task</em> doit automatiquement se remplir. Testez soigneusement les cas d’édition concurrente.</li>
<li><strong>Ignorer le pipeline de notifications</strong> : un rappel doit arriver sur le badge, en push et en email. Si vous n’implémentez qu’une seule méthode, l’expérience ne sera pas fidèle.</li>
<li><strong>Absence de mécanisme de verrouillage</strong> : sans cela, un utilisateur peut accidentellement supprimer une vue ou une propriété lors d’un glisser‑déposer maladroit.</li>
<li><strong>Mauvaise gestion de l’état « inline »</strong> : si la conversion d’une base pleine page en bloc inline ne préserve pas l’ensemble des vues et propriétés, les utilisateurs perdent leur configuration.</li>
</ul>
<hr>
<p>En suivant ces spécifications, vous reproduirez non seulement l’apparence mais aussi la logique profonde des bases de données Notion, où les tâches ne sont qu’une configuration enrichie du même moteur, agrémentée d’automatisations et de rappels avancés. La clé réside dans la séparation claire entre les données (propriétés), les vues (filtres/tris/groupes) et les interactions (glisser‑déposer, conversion inline), le tout chapeauté par un système de notifications multicanaux.</p>
<div class="sources-panel">
<details>
<summary>Sources (17)</summary>
<div class="sources-list">
<a href="https://www.notion.com/help/guides/full-page-vs-inline-databases" target="_blank" rel="noopener noreferrer"><span class="snum">1.</span><span>Full page vs. inline databases - Notion</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/intro-to-databases" target="_blank" rel="noopener noreferrer"><span class="snum">2.</span><span>Intro to databases – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/guides/creating-a-database" target="_blank" rel="noopener noreferrer"><span class="snum">3.</span><span>Creating a database - Notion</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/tables" target="_blank" rel="noopener noreferrer"><span class="snum">4.</span><span>Table view – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/layouts" target="_blank" rel="noopener noreferrer"><span class="snum">5.</span><span>Layouts – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/guides/databases-reimagined-whats-changed" target="_blank" rel="noopener noreferrer"><span class="snum">6.</span><span>Databases reimagined— what's changed? - Notion</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/galleries" target="_blank" rel="noopener noreferrer"><span class="snum">7.</span><span>Gallery view – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/guides/block-basics-build-the-foundation-for-your-teams-pages" target="_blank" rel="noopener noreferrer"><span class="snum">8.</span><span>Block basics: build the foundation for your team's pages - Notion</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/boards" target="_blank" rel="noopener noreferrer"><span class="snum">9.</span><span>Board view – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/lists" target="_blank" rel="noopener noreferrer"><span class="snum">10.</span><span>List view – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/database-properties" target="_blank" rel="noopener noreferrer"><span class="snum">11.</span><span>Database properties – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/customize-your-database" target="_blank" rel="noopener noreferrer"><span class="snum">12.</span><span>Database settings - Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/views-filters-and-sorts" target="_blank" rel="noopener noreferrer"><span class="snum">13.</span><span>Views, filters, sorts &amp; groups – Notion Help Center</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/help/guides/tasks-manageable-steps-sub-tasks-dependencies" target="_blank" rel="noopener noreferrer"><span class="snum">14.</span><span>Break tasks into manageable steps with sub-tasks and dependencies</span><span class="sdomain">notion.com</span></a>
<a href="https://www.notion.com/fr/help/reminders" target="_blank" rel="noopener noreferrer"><span class="snum">15.</span><span>Rappels – Centre d'aide Notion</span><span class="sdomain">notion.com</span></a>
<a href="https://blog.notionfacile.fr/comment-ajouter-des-rappels-dans-notion/" target="_blank" rel="noopener noreferrer"><span class="snum">16.</span><span>Comment ajouter des rappels dans Notion</span><span class="sdomain">blog.notionfacile.fr</span></a>
<a href="https://2sync.com/fr/blog/comment-definir-des-rappels-dans-notion" target="_blank" rel="noopener noreferrer"><span class="snum">17.</span><span>Comment configurer des rappels dans Notion (3 méthodes) - 2sync</span><span class="sdomain">2sync.com</span></a>
</div>
</details>
</div>
<div class="chat-cta"><button id="btn-chat-about" class="chat-cta-btn" data-research-id="rp-6f710e93cc05"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg><span>Discuss</span></button><div class="chat-cta-hint">Opens a new chat with this report as context.</div></div>
</main>
</div>
<div class="report-footer">
Generated by Odysseus Deep Research · July 09, 2026 at 20:36
</div>
<script>
(function() {
// ESC closes the report tab. window.close() works when the tab was
// opened via window.open() (which is how the panel launches it). If the
// browser blocks self-close (rare — e.g. report opened by direct URL),
// fall back to history.back() so ESC still feels responsive.
document.addEventListener('keydown', function(e) {
if (e.key !== 'Escape' || e.defaultPrevented) return;
// Don't hijack ESC while typing in a field or with an open dropdown.
var t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
var menu = document.getElementById('export-menu');
if (menu && menu.classList.contains('open')) { menu.classList.remove('open'); return; }
try { window.close(); } catch (err) {}
// window.close() is a no-op when the tab wasn't script-opened; in that
// case fall back to navigation so the key isn't ignored.
setTimeout(function() { if (!window.closed) history.back(); }, 50);
});
// Export dropdown toggle
var exportBtn = document.getElementById('btn-export');
var exportMenu = document.getElementById('export-menu');
exportBtn.addEventListener('click', function(e) {
e.stopPropagation();
exportMenu.classList.toggle('open');
});
document.addEventListener('click', function() { exportMenu.classList.remove('open'); });
// Save as PDF (browser print)
document.getElementById('btn-pdf').addEventListener('click', function() {
exportMenu.classList.remove('open');
window.print();
});
// Download HTML
document.getElementById('btn-html').addEventListener('click', function() {
exportMenu.classList.remove('open');
var blob = new Blob([document.documentElement.outerHTML], { type: 'text/html' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = document.title.replace(/[^a-z0-9]+/gi, '-').substring(0, 60) + '.html';
a.click();
});
// Per-image hide — fades the image out, then POSTs to the backend so
// future renders of this report skip the URL. Falls back to a silent
// no-op if there's no session_id (e.g. the report was opened from a
// saved-HTML download where the backend isn't reachable).
var __sessionId = "rp-6f710e93cc05";
// Unused scraped images — the reroll pool. Each is used at most once.
var __spareImages = ["https://www.notion.comhttps://img.youtube.com/vi/RNkpc84wYn8/maxresdefault.jpg", "https://images.ctfassets.net/spoqsaf9291f/XBxHGXXIDGndLluNvHEyC/29d5117216cfd3c2ca351ddd1d4f3351/page-blocks.png", "https://www.notion.comhttps://img.youtube.com/vi/_efKZce3eDY/maxresdefault.jpg", "https://www.notion.comhttps://img.youtube.com/vi/Qhvix00IE0k/maxresdefault.jpg", "https://images.ctfassets.net/spoqsaf9291f/2C9kI0iZiHR4iAHoV6AIRP/030a34f9a7ae10d7ea78274319a5a18d/Database_properties_-_hero.png", "https://images.ctfassets.net/spoqsaf9291f/3tW6M9iul7rd1kZOFwArh2/e463b72de22cc09f9fa47c1cd09c0ffe/Group_90.png", "https://images.ctfassets.net/spoqsaf9291f/OLBVmUUtVDV9UbCsemZc0/4f8ed5158e378ddaac560dcb3ce3066a/subtasks-dependencies.png", "https://images.ctfassets.net/spoqsaf9291f/4yzyxTI2A967HQ4H3l9qR9/f14d71ecec8f7583a0b7832f292529f4/Reference_Visuals_Reminders.png", "https://blog.notionfacile.fr/content/images/2026/03/comment-ajouter-des-rappels-dans-notion.webp", "https://media.2sync.com/blog/how-to-set-reminders-in-notion-2025.png"];
// Persist a rejected URL so future renders skip it.
function __persistHide(url) {
if (!__sessionId || !url) return;
fetch('/api/research/' + encodeURIComponent(__sessionId) + '/hide-image', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url }),
}).catch(function(err) { console.warn('hide-image POST failed', err); });
}
// Once the pool is empty, there's nothing to swap to — hide all reroll btns.
function __syncRerollAvailability() {
if (__spareImages.length === 0) {
document.querySelectorAll('.img-reroll-btn').forEach(function(b) { b.disabled = true; });
}
}
document.querySelectorAll('.img-hide-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault(); e.stopPropagation();
var wrap = btn.closest('[data-img-url]');
if (!wrap) return;
var url = wrap.dataset.imgUrl;
wrap.classList.add('fading');
setTimeout(function() { wrap.remove(); }, 280);
__persistHide(url);
});
});
// Reroll — swap the current image for the next unused scraped one, and
// persist-hide the rejected URL so it won't resurface on reload.
document.querySelectorAll('.img-reroll-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault(); e.stopPropagation();
// Per-button busy flag — a rapid double-click would otherwise both
// shift the spare pool, but only the second probe's image would land,
// silently consuming the first one. Bail until finish() clears it.
if (btn.dataset._busy === '1') return;
if (__spareImages.length === 0) { btn.disabled = true; return; }
var wrap = btn.closest('[data-img-url]');
if (!wrap) return;
var img = wrap.querySelector('img');
if (!img) return;
btn.dataset._busy = '1';
var oldUrl = wrap.dataset.imgUrl;
var newUrl = __spareImages.shift();
btn.classList.add('spinning');
// Swap once the new image has loaded (or failed) to avoid a flash of empty.
var probe = new Image();
var done = false;
var finish = function(ok) {
if (done) return; done = true;
btn.classList.remove('spinning');
delete btn.dataset._busy;
if (ok) {
img.src = newUrl;
wrap.dataset.imgUrl = newUrl;
__persistHide(oldUrl);
} else {
// Bad candidate — persist-hide it so it can't resurface on reload,
// then try the next spare if any remain. Busy flag already cleared
// so the synthetic click below proceeds.
__persistHide(newUrl);
if (__spareImages.length) btn.click();
}
__syncRerollAvailability();
};
probe.onload = function() { finish(true); };
probe.onerror = function() { finish(false); };
probe.src = newUrl;
});
});
__syncRerollAvailability();
// "Show hidden (N)" button — clears the hidden_images list on the
// server, then reloads the page so all images come back.
var restoreBtn = document.getElementById('btn-restore-images');
if (restoreBtn && __sessionId) {
restoreBtn.addEventListener('click', function() {
restoreBtn.disabled = true;
restoreBtn.textContent = 'Restoring…';
fetch('/api/research/' + encodeURIComponent(__sessionId) + '/unhide-images', {
method: 'POST', credentials: 'same-origin',
}).then(function() { window.location.reload(); })
.catch(function(err) {
restoreBtn.disabled = false;
restoreBtn.textContent = 'Failed — retry?';
console.warn('unhide-images POST failed', err);
});
});
}
// TOC: explicit smooth-scroll handler (some browsers/anchor plugins
// bypass the CSS `scroll-behavior: smooth` rule on hash clicks).
// Also keeps the URL hash updated and toggles an `.active` highlight.
var tocLinks = document.querySelectorAll('.toc-sidebar nav a[href^="#"]');
tocLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
var id = link.getAttribute('href').slice(1);
var target = document.getElementById(id);
if (!target) return;
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
history.replaceState(null, '', '#' + id);
});
});
// Highlight the TOC entry that matches whichever heading is currently
// closest to the top of the viewport. IntersectionObserver keeps it
// cheap (no scroll listener spam).
var tocMap = {};
tocLinks.forEach(function(link) {
tocMap[link.getAttribute('href').slice(1)] = link;
});
var activeId = null;
function setActive(id) {
if (id === activeId) return;
if (activeId && tocMap[activeId]) tocMap[activeId].classList.remove('active');
if (id && tocMap[id]) tocMap[id].classList.add('active');
activeId = id;
}
var headings = document.querySelectorAll('.content h2[id], .content h3[id]');
if (headings.length && 'IntersectionObserver' in window) {
var visible = new Set();
var io = new IntersectionObserver(function(entries) {
entries.forEach(function(en) {
if (en.isIntersecting) visible.add(en.target.id);
else visible.delete(en.target.id);
});
// Pick the visible heading that's furthest down in document order
// before the current scroll — i.e. the section we're reading.
var current = null;
for (var i = 0; i < headings.length; i++) {
if (visible.has(headings[i].id)) { current = headings[i].id; break; }
}
if (current) setActive(current);
}, { rootMargin: '-10% 0px -75% 0px', threshold: 0 });
headings.forEach(function(h) { io.observe(h); });
}
// Chat about this research — POST to spinoff and redirect to the new chat
var chatBtn = document.getElementById('btn-chat-about');
if (chatBtn) {
chatBtn.addEventListener('click', function() {
var researchId = chatBtn.dataset.researchId;
if (!researchId) return;
var origLabel = chatBtn.innerHTML;
chatBtn.disabled = true;
chatBtn.innerHTML = '<span>Creating chat…</span>';
fetch('/api/research/spinoff/' + encodeURIComponent(researchId), {
method: 'POST', credentials: 'same-origin',
}).then(function(res) {
if (!res.ok) {
return res.json().then(function(d) {
throw new Error(d && d.detail ? d.detail : ('HTTP ' + res.status));
}, function() { throw new Error('HTTP ' + res.status); });
}
return res.json();
}).then(function(data) {
if (!data || !data.session_id) {
throw new Error('Server did not return a session id');
}
var url = '/#' + data.session_id;
var opened = false;
// The report typically opens in a new tab — if we have access to the
// original Odysseus tab, navigate it and close this report tab so the
// user lands directly in the new chat.
try {
if (window.opener && !window.opener.closed) {
window.opener.location.href = url;
window.opener.location.reload();
window.opener.focus();
opened = true;
window.close();
}
} catch (e) { /* cross-origin or detached opener — fall through */ }
if (!opened) {
// No opener (report was opened directly via URL) — open the chat in a
// new tab so the report stays available.
var w = window.open(url, '_blank');
if (w) {
chatBtn.disabled = false;
chatBtn.innerHTML = '<span>Chat opened in new tab</span>';
} else {
// Popup blocked — navigate this tab as a last resort.
window.location.href = url;
window.location.reload();
}
}
}).catch(function(err) {
chatBtn.disabled = false;
chatBtn.innerHTML = origLabel;
alert('Could not start follow-up chat: ' + err.message);
});
});
}
})();
// Colorize comparison table cells
if (document.body.classList.contains('category-comparison')) {
const pos = /^(yes|excellent|best|great|strong|fast|high|superior|winner|free|unlimited|native|full|advanced|built[- ]in|✓|✅|⭐)/i;
const neg = /^(no|none|poor|weak|slow|low|limited|lacking|missing|basic|minimal|✗|❌|N\/A$)/i;
const mid = /^(moderate|average|fair|partial|some|decent|okay|mixed|varies|depends)/i;
document.querySelectorAll('.content table td').forEach(td => {
if (td.cellIndex === 0) return;
const t = td.textContent.trim();
if (pos.test(t)) td.classList.add('cmp-pos');
else if (neg.test(t)) td.classList.add('cmp-neg');
else if (mid.test(t)) td.classList.add('cmp-mid');
});
}
</script>
</body></html>