From c80aa67c508311c47da8a2d0ba86fde3a2802f12 Mon Sep 17 00:00:00 2001 From: Cedric Date: Mon, 15 Jun 2026 22:26:57 +0200 Subject: [PATCH] =?UTF-8?q?Premier=20import=20de=20mon=20site=20Mon=20Cin?= =?UTF-8?q?=C3=A9ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/dashboard.html | 241 ++++++++++++ admin/login.html | 16 + css/admin.css | 145 ++++++++ css/public.css | 603 ++++++++++++++++++++++++++++++ index.html | 91 +++++ js/admin.js | 862 +++++++++++++++++++++++++++++++++++++++++++ js/public.js | 265 +++++++++++++ 7 files changed, 2223 insertions(+) create mode 100644 admin/dashboard.html create mode 100644 admin/login.html create mode 100644 css/admin.css create mode 100644 css/public.css create mode 100644 index.html create mode 100644 js/admin.js create mode 100644 js/public.js diff --git a/admin/dashboard.html b/admin/dashboard.html new file mode 100644 index 0000000..04d01b8 --- /dev/null +++ b/admin/dashboard.html @@ -0,0 +1,241 @@ + + + + + + + + Dashboard + + + + +
+
+
+

Backoffice

+
+ + + + + + + + + + + + + + + + +
+
+ +
+ + +
+ +
+ Clé API TMDB requise pour la recherche d'affiches.
+ Gratuit sur themoviedb.org/settings/api (inscription gratuite, clé disponible immédiatement).
+
+ + +
+
+ +
+ + + + + + + + + + + + + +
+ + AfficheTitreAnnéeRéalisateurNoteActions
+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/admin/login.html b/admin/login.html new file mode 100644 index 0000000..a70bdec --- /dev/null +++ b/admin/login.html @@ -0,0 +1,16 @@ + + + + +Login + +
+ +
+ + \ No newline at end of file diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..9983ddc --- /dev/null +++ b/css/admin.css @@ -0,0 +1,145 @@ +* { box-sizing: border-box; margin: 0; padding: 0; } +:root { + --gold: #C9A84C; --gold-light: #E8C97A; + --dark: #0E0E0E; --surface: #1A1A1A; --surface2: #252525; + --text: #F0ECE3; --muted: #A8A4A0; --border: rgba(201,168,76,0.2); + --red: #c0392b; +} +body { background: var(--dark); color: var(--text); font-family: 'DM Sans', sans-serif; min-height: 100vh; } + +/* ── PAGES ── */ +.page { display: none; } +.page.active { display: block; } + +/* ── SITE PUBLIC ── */ +.site-wrap { max-width: 920px; margin: 0 auto; padding: 2rem 1.2rem 4rem; } +header { text-align: center; padding: 3rem 0 2rem; border-bottom: 1px solid var(--border); margin-bottom: 2.5rem; } +header::before { content: ''; display: block; width: 60px; height: 2px; background: var(--gold); margin: 0 auto 1.5rem; } +.site-title { font-family: 'Playfair Display', serif; font-size: 2.8rem; letter-spacing: 0.05em; } +.site-title span { color: var(--gold); font-style: italic; } +.site-subtitle { font-size: 0.78rem; letter-spacing: 0.25em; color: var(--muted); text-transform: uppercase; margin-top: 0.6rem; } +.pub-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.8rem; } +.count { font-size: 0.78rem; letter-spacing: 0.1em; color: #C0BCB8; text-transform: uppercase; } + +.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1.5rem; } +.card { background: var(--surface); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; cursor: pointer; transition: transform 0.2s, border-color 0.2s; } +.card:hover { transform: translateY(-5px); border-color: var(--gold); } +.poster-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; background: var(--surface2); } +.poster-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; } +.poster-ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 0.5rem; color: var(--muted); font-size: 0.75rem; } +.poster-ph i { font-size: 2.5rem; } +.rating-badge { position: absolute; top: 8px; right: 8px; background: rgba(14,14,14,0.85); border: 1px solid var(--gold); color: var(--gold); font-family: 'Playfair Display', serif; font-size: 1rem; font-weight: 700; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } +.card-body { padding: 0.9rem 1rem 1rem; } +.card-title { font-family: 'Playfair Display', serif; font-size: 1rem; line-height: 1.3; margin-bottom: 0.2rem; } +.card-year { font-size: 0.75rem; color: #B8B4B0; margin-bottom: 0.4rem; } +.card-stars { color: var(--gold); font-size: 0.8rem; letter-spacing: 0.1em; margin-bottom: 0.4rem; } +.card-excerpt { font-size: 0.78rem; color: #C0BCB8; line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } + +.empty-state { text-align: center; padding: 5rem 1rem; color: #C8C4C0; } +.empty-state i { font-size: 3.5rem; display: block; margin-bottom: 1rem; opacity: 0.3; } +.empty-state p { font-size: 0.95rem; line-height: 1.7; } + +footer { text-align: center; margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.75rem; color: var(--muted); letter-spacing: 0.1em; } + +/* ── BACKOFFICE ── */ +.admin-wrap { max-width: 960px; margin: 0 auto; padding: 2rem 1.2rem 4rem; } +.admin-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: 1.2rem; border-bottom: 1px solid var(--border); margin-bottom: 2rem; flex-wrap: wrap; gap: 1rem; } +.admin-title { font-family: 'Playfair Display', serif; font-size: 1.6rem; } +.admin-title span { color: var(--gold); } +.admin-actions { display: flex; gap: 0.7rem; align-items: center; } + +.btn { display: inline-flex; align-items: center; gap: 6px; padding: 0.55rem 1.1rem; font-family: 'DM Sans', sans-serif; font-size: 0.82rem; font-weight: 500; cursor: pointer; border-radius: 2px; letter-spacing: 0.04em; transition: background 0.2s, color 0.2s; border: none; } +.btn-gold { background: var(--gold); color: var(--dark); } +.btn-gold:hover { background: var(--gold-light); } +.btn-outline { background: transparent; border: 1px solid rgba(201,168,76,0.35); color: #C8C4BE; } +.btn-outline:hover { border-color: var(--gold); color: var(--gold); } +.btn-danger { background: transparent; border: 1px solid #555; color: #C0BBBB; } +.btn-danger:hover { border-color: var(--red); color: var(--red); } + +/* Admin table */ +.admin-table-wrap { overflow-x: auto; } +.admin-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } +.admin-table th { text-align: left; font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: #B8B4B0; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--border); font-weight: 600; } +.admin-table td { padding: 0.75rem 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.04); vertical-align: middle; } +.admin-table tr:hover td { background: var(--surface2); } +.thumb { width: 36px; height: 54px; object-fit: cover; border-radius: 2px; display: block; background: var(--surface2); } +.thumb-ph { width: 36px; height: 54px; background: var(--surface2); border-radius: 2px; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 1rem; } +.tbl-stars { color: var(--gold); font-size: 0.75rem; letter-spacing: 0.05em; } +.tbl-actions { display: flex; gap: 0.5rem; } +.tbl-actions button { background: none; border: none; cursor: pointer; color: #B0ACAA; font-size: 1rem; padding: 4px; border-radius: 2px; transition: color 0.15s; } +.tbl-actions button:hover { color: var(--gold); } +.tbl-actions button.del:hover { color: var(--red); } + +/* ── MODAL ── */ +.overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.88); z-index: 200; align-items: center; justify-content: center; padding: 1rem; } +.overlay.open { display: flex; } +.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; width: 100%; max-width: 540px; max-height: 92vh; overflow-y: auto; padding: 2rem; position: relative; } +.modal-close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; color: var(--muted); cursor: pointer; font-size: 1.3rem; line-height: 1; } +.modal-close:hover { color: var(--text); } +.modal-h { font-family: 'Playfair Display', serif; font-size: 1.35rem; color: var(--gold); margin-bottom: 1.4rem; } + +/* Film search */ +.search-row { display: flex; gap: 0.6rem; margin-bottom: 1rem; } +.search-row input { flex: 1; background: var(--surface2); border: 1px solid rgba(255,255,255,0.1); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; padding: 0.65rem 0.8rem; border-radius: 3px; outline: none; } +.search-row input:focus { border-color: var(--gold); } +.btn-search { background: var(--gold); color: var(--dark); border: none; padding: 0 1rem; border-radius: 3px; cursor: pointer; font-size: 1rem; transition: background 0.2s; } +.btn-search:hover { background: var(--gold-light); } + +.search-results { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 0.7rem; margin-bottom: 1.2rem; max-height: 220px; overflow-y: auto; } +.sr-item { cursor: pointer; border: 2px solid transparent; border-radius: 3px; overflow: hidden; transition: border-color 0.15s; } +.sr-item:hover, .sr-item.selected { border-color: var(--gold); } +.sr-item img { width: 100%; aspect-ratio: 2/3; object-fit: cover; display: block; } +.sr-item p { font-size: 0.65rem; color: var(--muted); padding: 3px 4px; line-height: 1.3; text-align: center; background: var(--surface2); } +.search-hint { font-size: 0.78rem; color: #B8B4B0; margin-bottom: 1rem; } +.searching { font-size: 0.8rem; color: var(--muted); text-align: center; padding: 1rem 0; } + +.form-group { margin-bottom: 1rem; } +.form-group label { display: block; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: #C0BCB8; margin-bottom: 0.35rem; } +.form-group input, .form-group textarea { width: 100%; background: var(--surface2); border: 1px solid rgba(255,255,255,0.1); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; padding: 0.65rem 0.8rem; border-radius: 3px; outline: none; transition: border-color 0.2s; } +.form-group input:focus, .form-group textarea:focus { border-color: var(--gold); } +.form-group textarea { resize: vertical; min-height: 100px; } +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } + +.star-select { display: flex; gap: 6px; } +.star-select button { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--muted); transition: color 0.15s, transform 0.1s; padding: 2px; line-height: 1; } +.star-select button.active { color: var(--gold); } +.star-select button:hover { transform: scale(1.15); } + +.poster-preview { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; } +.poster-preview img { width: 60px; height: 90px; object-fit: cover; border-radius: 3px; border: 1px solid var(--border); } +.poster-preview-none { width: 60px; height: 90px; background: var(--surface2); border-radius: 3px; border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 1.3rem; } +.poster-preview-info { font-size: 0.8rem; color: #B8B4B0; line-height: 1.6; } +.poster-preview-info strong { color: var(--text); font-weight: 500; } + +.btn-save { width: 100%; background: var(--gold); color: var(--dark); border: none; padding: 0.8rem; font-family: 'DM Sans', sans-serif; font-size: 0.9rem; font-weight: 500; cursor: pointer; border-radius: 3px; margin-top: 0.5rem; transition: background 0.2s; } +.btn-save:hover { background: var(--gold-light); } +.divider { border: none; border-top: 1px solid var(--border); margin: 1.2rem 0; } + +/* Detail overlay */ +.detail-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 200; align-items: flex-start; justify-content: center; overflow-y: auto; padding: 3rem 1rem; } +.detail-overlay.open { display: flex; } +.detail-modal { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; width: 100%; max-width: 680px; padding: 2rem; position: relative; display: grid; grid-template-columns: 160px 1fr; gap: 2rem; align-items: start; } +@media(max-width:520px){ .detail-modal { grid-template-columns: 1fr; } } +.detail-poster { aspect-ratio: 2/3; background: var(--surface2); border-radius: 4px; overflow: hidden; } +.detail-poster img { width: 100%; height: 100%; object-fit: cover; display: block; } +.detail-title { font-family: 'Playfair Display', serif; font-size: 1.7rem; line-height: 1.2; margin-bottom: 0.3rem; } +.detail-meta { font-size: 0.8rem; color: #C0BCB8; margin-bottom: 0.8rem; } +.detail-stars { color: var(--gold); font-size: 1.1rem; letter-spacing: 0.15em; margin-bottom: 1.2rem; } +.detail-review { font-size: 0.95rem; line-height: 1.85; color: #D0CBC1; font-family: 'Playfair Display', serif; font-style: italic; } +.detail-review::before { content: '\201C'; font-size: 3.5rem; color: var(--gold); line-height: 0; vertical-align: -1rem; margin-right: 0.15rem; opacity: 0.6; } + +/* Admin login */ +.login-wrap { max-width: 360px; margin: 6rem auto; padding: 1.5rem; } +.login-wrap h2 { font-family: 'Playfair Display', serif; color: var(--gold); margin-bottom: 1.5rem; font-size: 1.4rem; } +.login-wrap input { width: 100%; background: var(--surface2); border: 1px solid rgba(255,255,255,0.1); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; padding: 0.65rem 0.8rem; border-radius: 3px; outline: none; margin-bottom: 1rem; } +.login-wrap input:focus { border-color: var(--gold); } +.login-err { color: var(--red); font-size: 0.82rem; margin-bottom: 0.8rem; display: none; } + +/* TMDB badge */ +.tmdb-note { font-size: 0.72rem; color: var(--muted); margin-top: 0.4rem; } +.tmdb-note a { color: var(--gold); text-decoration: none; } + +/* API key notice */ +.api-notice { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 1rem 1.2rem; margin-bottom: 1.5rem; font-size: 0.82rem; color: #C0BCB8; line-height: 1.6; } +.api-notice strong { color: var(--text); } +.api-notice a { color: var(--gold); } \ No newline at end of file diff --git a/css/public.css b/css/public.css new file mode 100644 index 0000000..67b4483 --- /dev/null +++ b/css/public.css @@ -0,0 +1,603 @@ +/* --- VARIABLES DE DESIGN ET PALETTE SOMBRE --- */ +:root { + --background: #0a0a0a; + --surface: #1a1a1a; + --surface-card: #1e1e1e; + --surface-hover: #272727; + --border: #2e2e2e; + --border-hover: #444444; + --text: #ffffff; + --text-secondary: #e4e4e7; + --muted: #aaaab8; + --gold: #C9A84C; + --gold-glow: rgba(201, 168, 76, 0.2); + --gold-dim: rgba(201, 168, 76, 0.08); + --shadow-cinematic: 0 15px 35px rgba(0, 0, 0, 0.7); + --shadow-overlay: 0 30px 70px rgba(0, 0, 0, 0.9); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + + +/* page visibility (used by index.html wrapper) */ +.page { display: none; } +.page.active { display: block; } + +body { + background-color: var(--background); + color: var(--text); + font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif; + -webkit-font-smoothing: antialiased; + padding: 3rem 1rem; +} + +.site-wrap { + max-width: 1040px; + margin: 0 auto; +} + +/* --- EN-TÊTE CENTRÉ --- */ +.site-header { + text-align: center; + margin-bottom: 3.5rem; +} + +.site-title { + font-family: 'Playfair Display', Georgia, serif; + font-size: 3.2rem; + font-weight: 700; + letter-spacing: -0.03em; + margin-bottom: 0.6rem; +} + +.site-title span { + color: var(--gold); + text-shadow: 0 0 20px var(--gold-glow); +} + +.site-subtitle { + color: #c0c0cc; + font-size: 0.95rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +/* --- NAVIGATION PAR ONGLETS --- */ +.pub-tabs-container { + display: flex; + justify-content: center; + margin-bottom: 3rem; +} + +.pub-tabs { + background-color: var(--surface); + border: 1px solid var(--border); + padding: 0.4rem; + border-radius: 30px; + display: inline-flex; + gap: 0.4rem; +} + +.tab-btn { + background: none; + border: none; + color: #d4d4de; + font-family: 'DM Sans', sans-serif; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + padding: 0.65rem 1.6rem; + border-radius: 25px; + display: flex; + align-items: center; + gap: 0.6rem; + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.tab-btn i { font-size: 1.15rem; } + +.tab-btn:hover { + color: var(--text); + background-color: var(--surface-hover); +} + +.tab-btn.active { + color: var(--background); + background-color: var(--gold); + font-weight: 600; + box-shadow: 0 4px 15px var(--gold-glow); +} + +/* --- BARRE D'OUTILS --- */ +.pub-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + border-bottom: 1px solid var(--border); + padding-bottom: 1rem; +} + +.count { + font-size: 0.9rem; + color: #ccccd6; + font-weight: 500; +} + +.btn-admin-link { + color: #d4b05a !important; + text-decoration: none; + font-size: 0.8rem; + letter-spacing: 0.06em; + text-transform: uppercase; + display: inline-flex; + align-items: center; + gap: 0.5rem; + border: 1px solid rgba(201, 168, 76, 0.2); + padding: 0.5rem 1rem; + border-radius: 4px; + transition: all 0.25s ease; +} + +.btn-admin-link:hover { + background: var(--gold-dim); + border-color: var(--gold); +} + +/* --- GRILLE PRINCIPALE ET CARTES --- */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 2.5rem 1.8rem; + margin-top: 1rem; +} + +.card { + background: transparent; + cursor: pointer; + display: flex; + flex-direction: column; +} + +.card-poster-wrap { + width: 100%; + aspect-ratio: 2 / 3; + position: relative; + border-radius: 8px; + overflow: hidden; + background: #121212; + box-shadow: var(--shadow-cinematic); + border: 1px solid var(--border); + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} + +.card-poster { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1); +} + +.card-poster-wrap::after { + content: ''; + position: absolute; + top: 0; left: -150%; width: 120%; height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent); + transform: skewX(-20deg); +} + +.card:hover .card-poster-wrap { + transform: translateY(-8px); + border-color: var(--border-hover); + box-shadow: 0 20px 30px rgba(0, 0, 0, 0.8); +} + +.card:hover .card-poster-wrap::after { + left: 150%; + transition: all 0.75s ease; +} + +.card:hover .card-poster { transform: scale(1.05); } +.card:hover .card-title { color: var(--gold); } + +.card-poster-placeholder { + width: 100%; height: 100%; + display: flex; flex-direction: column; justify-content: center; align-items: center; + padding: 1.2rem; text-align: center; + background: linear-gradient(135deg, #181818, #0a0a0a); +} + +.card-poster-placeholder i { font-size: 2.5rem; color: var(--gold); margin-bottom: 0.7rem; } +.card-body { padding: 0.9rem 0 0 0; } + +.card-title { + font-size: 0.95rem; margin: 0 0 0.3rem 0; font-weight: 600; color: #ffffff; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + transition: color 0.25s ease; +} + +.card-meta { font-size: 0.8rem; color: #c4c4d0; font-weight: 500; } +.card-stars { + color: #FFD700; + margin-top: 0.4rem; + font-size: 0.85rem; + letter-spacing: 1px; +} + +.card-stars .stars-muted { + color: #ffffff; + opacity: 0.45; +} + +.card-video-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; } + +.badge-format { + background: var(--gold-dim); color: var(--gold); font-size: 0.7rem; font-weight: bold; + padding: 0.15rem 0.5rem; border-radius: 3px; border: 1px solid rgba(201, 168, 76, 0.2); + text-transform: uppercase; +} + +.video-length { color: #a8a8b8; font-size: 0.75rem; display: inline-flex; align-items: center; gap: 0.25rem; } + +/* ========================================================================== + REFONTE IMMERSIVE DE LA POP-UP (MODALE PREMIUM) + ========================================================================== */ +.detail-overlay { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(3, 3, 3, 0.85); + backdrop-filter: blur(25px); + -webkit-backdrop-filter: blur(25px); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + padding: 2rem 1rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} + +.detail-overlay.open { + opacity: 1; + pointer-events: all; +} + +.detail-modal { + background: #0f0f10; + border: 1px solid var(--border); + border-radius: 16px; + max-width: 840px; + width: 100%; + max-height: 85vh; + overflow-y: auto; + position: relative; + display: flex; + gap: 3.5rem; + padding: 3.5rem; + box-shadow: var(--shadow-overlay); + transform: scale(0.96) translateY(20px); + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.detail-overlay.open .detail-modal { + transform: scale(1) translateY(0); +} + +.detail-modal.no-poster { + max-width: 620px; + display: block; +} + +/* Bouton Fermer */ +.modal-close { + position: absolute; + top: 1.5rem; right: 1.5rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + color: #d0d0da; + width: 38px; height: 38px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 1.1rem; cursor: pointer; + transition: all 0.25s ease; + z-index: 10; +} + +.modal-close:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.08); + border-color: #444; + transform: scale(1.05); +} + +/* Colonne Image (Affiche) */ +.detail-poster { + flex: 0 0 240px; + width: 240px; + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 20px 45px rgba(0, 0, 0, 0.8); + background: #141414; + align-self: flex-start; +} + +.detail-poster.poster-critique { aspect-ratio: unset; } +.detail-poster.poster-critique img { width: 100%; height: auto; display: block; } + +.detail-poster.poster-video { aspect-ratio: 2 / 3; display: flex; align-items: center; justify-content: center; } +.detail-poster.poster-video img { width: 100%; height: 100%; object-fit: contain; display: block; } + +/* Colonne Contenu */ +.detail-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.detail-header-block { + margin-bottom: 1.8rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + padding-bottom: 1.2rem; +} + +.detail-title { + font-family: 'Playfair Display', Georgia, serif; + font-size: 2.4rem; + font-weight: 700; + margin-bottom: 0.5rem; + line-height: 1.2; + color: var(--text); +} + +.detail-meta { + font-size: 0.95rem; + color: #d0d0da; + margin-bottom: 0.8rem; + font-weight: 500; + letter-spacing: 0.02em; +} + +/* --- VISIBILITÉ DES ÉTOILES DANS LA POP-UP --- */ +.detail-stars { + color: #FFD700; /* Un or plus lumineux et chaud (YellowGold) */ + font-size: 1.6rem; /* Taille augmentée pour une meilleure accroche visuelle */ + letter-spacing: 0.12em; /* Espace subtil entre les étoiles */ + margin-bottom: 1.2rem; + /* Effet de néon cinématographique discret sur les étoiles pleines */ + text-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 20px rgba(255, 215, 0, 0.2); + display: inline-flex; + align-items: center; +} + +/* Style appliqué aux étoiles vides (masquées en arrière-plan) */ +.detail-stars .stars-muted { + color: #ffffff; + opacity: 0.35; /* Très estompé dans le noir pour faire ressortir la note réelle */ + text-shadow: none; /* Pas d'effet de lueur pour les étoiles vides */ +} + +/* --- DESIGN SUBLIMÉ DE LA CRITIQUE --- */ +.pub-review-card { + position: relative; + background: linear-gradient(145deg, #151516, #111112); + border: 1px solid var(--border); + border-radius: 12px; + padding: 2.2rem 2rem 2rem 2rem; + margin-top: 0.5rem; + box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.03); +} + +.pub-review-quote-icon { + position: absolute; + top: -14px; left: 24px; + background: #181819; + border: 1px solid var(--border); + color: var(--gold); + width: 30px; height: 30px; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 0.95rem; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4); +} + +.pub-review-text { + font-family: 'DM Sans', sans-serif; + color: var(--text-secondary); + font-size: 1.05rem; + line-height: 1.8; + white-space: pre-wrap; +} + +/* --- DESIGN INTERNE : VIDEOTHEQUE --- */ +.pub-tech-badges { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + margin-bottom: 1.5rem; +} + +.tech-pill { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + padding: 0.38rem 0.85rem; + border-radius: 6px; + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + color: #dddddd; + letter-spacing: 0.02em; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.tech-pill.format-gold { + background: var(--gold-dim); + border-color: rgba(201, 168, 76, 0.3); + color: var(--gold); +} + +.pub-tech-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + background: #141415; + border: 1px solid var(--border); + padding: 1.1rem 1.3rem; + border-radius: 8px; + margin-bottom: 1.8rem; +} + +.tech-meta-item { display: flex; flex-direction: column; gap: 0.25rem; } +.tech-meta-label { font-size: 0.75rem; text-transform: uppercase; color: #c0c0cc; letter-spacing: 0.05em; font-weight: 500; } +.tech-meta-value { font-size: 0.9rem; color: var(--text); font-weight: 500; } + +.pub-synopsis-box h4 { + color: var(--text); margin-bottom: 0.6rem; font-size: 0.85rem; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; +} + +.pub-synopsis-box p { color: #e0e0e0; font-size: 0.95rem; line-height: 1.7; white-space: pre-wrap; } + +/* --- FIN DE PAGE & ACCESSOIRES --- */ +footer { + margin-top: 6rem; border-top: 1px solid var(--border); + padding-top: 2rem; text-align: center; color: var(--muted); + font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; +} + +.empty-state { text-align: center; padding: 6rem 1rem; color: var(--muted); } +.empty-state i { font-size: 3.5rem; color: #444450; margin-bottom: 1.2rem; display: block; } + +/* ========================================================================== + COMPORTEMENT RESPONSIVE MOBILE (APPLICATION NATIVE APP-LIKE) + ========================================================================== */ +@media (max-width: 720px) { + body { padding: 2rem 1rem; } + .site-title { font-size: 2.6rem; } + + .detail-overlay { padding: 0; } + + .detail-modal { + flex-direction: column; + gap: 1.5rem; + padding: 4.5rem 1.5rem 2.5rem 1.5rem; + max-height: 100vh; + height: 100vh; + border-radius: 0; + border: none; + } + + .detail-poster { + margin: 0 auto; + width: 170px; + flex: 0 0 auto; + } + + .detail-header-block { + text-align: center; + padding-bottom: 1.5rem; + } + + .detail-title { font-size: 2rem; } + .detail-stars { justify-content: center; display: flex; } + + .pub-review-card { padding: 1.8rem 1.3rem; } + .pub-tech-grid { grid-template-columns: 1fr; gap: 0.9rem; } + + .pub-tabs { width: 100%; justify-content: center; } + .tab-btn { padding: 0.6rem 1.1rem; font-size: 0.85rem; } +} +/* ========================================================================== + FILTRE PAR NOTES (ÉTOILES) + ========================================================================== */ +.rating-filter-bar { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.rf-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #ccccda; + font-weight: 500; +} + +.rf-buttons { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; + justify-content: center; +} + +.rating-filter-btn { + background: #1e1e1e; + border: 1px solid #3a3a3a; + border-radius: 8px; + padding: 0.45rem 0.85rem; + cursor: pointer; + display: flex; + gap: 1px; + align-items: center; + transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); +} + +.rating-filter-btn:hover { + border-color: var(--gold); + background: var(--gold-dim); +} + +.rating-filter-btn.active { + border-color: var(--gold); + background: var(--gold-dim); + box-shadow: 0 0 12px var(--gold-glow); +} + +.rf-star { + font-size: 0.85rem; + color: #7a7a88; + transition: color 0.2s ease, text-shadow 0.2s ease; + line-height: 1; +} + +.rating-filter-btn:hover .rf-star, +.rating-filter-btn.active .rf-star { + color: #FFD700; + text-shadow: 0 0 6px rgba(255, 215, 0, 0.5); +} + +/* On hover: light up stars progressively via sibling trick */ +.rating-filter-btn:not(.active):hover .rf-star:not(.filled) { + color: rgba(255, 215, 0, 0.35); +} + +/* Active state: filled stars glow, empty ones stay dim */ +.rating-filter-btn.active .rf-star.filled { + color: #FFD700; + text-shadow: 0 0 8px rgba(255, 215, 0, 0.6); +} + +.rating-filter-btn.active .rf-star:not(.filled) { + color: #555560; + text-shadow: none; +} + +@media (max-width: 720px) { + .rating-filter-bar { gap: 0.7rem; } + .rf-label { display: none; } + .rf-star { font-size: 0.8rem; } + .rating-filter-btn { padding: 0.4rem 0.65rem; } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..785c5af --- /dev/null +++ b/index.html @@ -0,0 +1,91 @@ + + + + + + Mon Cinéma + + + + + +
+
+ + + +
+
+ + +
+
+ +
+ Filtrer : +
+ + + + + +
+
+ +
+ 0 critique + + Administration + +
+ +
+ + +
Mon Cinéma — Journal personnel
+
+
+ + + + + + \ No newline at end of file diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..1d070ec --- /dev/null +++ b/js/admin.js @@ -0,0 +1,862 @@ +const ADMIN_PASSWORD = 'cinema2024'; +const STORAGE_KEY = 'mon-cinema-films'; + +let films = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); +let currentRating = 0; +let editingFilmId = null; +let currentTab = 'critique'; + +function persist() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(films)); +} + +if (location.pathname.includes('dashboard.html') && sessionStorage.getItem('admin-auth') !== '1') { + window.location.href = 'login.html'; +} + +function doLogin() { + const val = document.getElementById('login-pwd').value; + if (val === ADMIN_PASSWORD) { + sessionStorage.setItem('admin-auth', '1'); + window.location.href = 'dashboard.html'; + } else { + document.getElementById('login-err').style.display = 'block'; + } +} + +function switchTab(tabName) { + currentTab = tabName; + + document.getElementById('tab-critiques').classList.toggle('active', tabName === 'critique'); + document.getElementById('tab-videotheque').classList.toggle('active', tabName === 'videotheque'); + + const thDynamic = document.getElementById('th-dynamic-col'); + if (thDynamic) { + thDynamic.textContent = tabName === 'critique' ? 'Note' : 'Format / Éditeur'; + } + + const btnCritiqueExport = document.getElementById('actions-critiques-export'); + const btnCritiqueImport = document.getElementById('actions-critiques-import'); + const btnVideoExport = document.getElementById('actions-video-export'); + const btnVideoImport = document.getElementById('actions-video-import'); + + if (tabName === 'videotheque') { + if (btnCritiqueExport) btnCritiqueExport.style.display = 'none'; + if (btnCritiqueImport) btnCritiqueImport.style.display = 'none'; + if (btnVideoExport) btnVideoExport.style.display = 'inline-flex'; + if (btnVideoImport) btnVideoImport.style.display = 'inline-flex'; + } else { + if (btnCritiqueExport) btnCritiqueExport.style.display = 'inline-flex'; + if (btnCritiqueImport) btnCritiqueImport.style.display = 'inline-flex'; + if (btnVideoExport) btnVideoExport.style.display = 'none'; + if (btnVideoImport) btnVideoImport.style.display = 'none'; + } + + renderAdminTable(); +} + +function handleTypeChange(typeValue) { + const videoFields = document.getElementById('form-group-videotheque-fields'); + const critiqueFields = document.getElementById('form-group-critique-fields'); + + if (typeValue === 'videotheque') { + if (videoFields) videoFields.style.display = 'block'; + if (critiqueFields) critiqueFields.style.display = 'none'; + } else { + if (videoFields) videoFields.style.display = 'none'; + if (critiqueFields) critiqueFields.style.display = 'block'; + } +} + +function setRating(n) { + currentRating = n; + const buttons = document.querySelectorAll('#star-select button'); + buttons.forEach((btn, idx) => { + btn.style.color = idx < n ? 'var(--gold)' : 'var(--muted)'; + }); +} + +function resetStars() { + currentRating = 0; + const buttons = document.querySelectorAll('#star-select button'); + buttons.forEach(btn => btn.style.color = 'var(--muted)'); +} + +function saveTmdbKey() { + const key = document.getElementById('tmdb-key-input').value.trim(); + localStorage.setItem('tmdb-api-key', key); + alert('Clé TMDB enregistrée avec succès !'); +} + +async function searchTMDB() { + const query = document.getElementById('search-input').value.trim(); + const key = localStorage.getItem('tmdb-api-key') || document.getElementById('tmdb-key-input').value.trim(); + const resultsWrap = document.getElementById('search-results-wrap'); + + if (!key) { + alert("Veuillez configurer votre clé API TMDB d'abord."); + return; + } + if (!query) return; + + resultsWrap.innerHTML = '
Recherche en cours...
'; + + try { + const res = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(query)}&language=fr-FR`); + const data = await res.json(); + + if (!data.results || data.results.length === 0) { + resultsWrap.innerHTML = '
Aucun résultat trouvé.
'; + return; + } + + resultsWrap.innerHTML = ` +
+ ${data.results.slice(0, 5).map(movie => { + const posterUrl = movie.poster_path ? `https://image.tmdb.org/t/p/w300${movie.poster_path}` : ''; + const year = movie.release_date ? movie.release_date.substring(0, 4) : ''; + const safeTitle = movie.title.replace(/'/g, "\\'").replace(/"/g, """); + return ` +
+ ${posterUrl ? `` : `
`} + ${movie.title} (${year}) +
+ `; + }).join('')} +
+ `; + } catch (err) { + resultsWrap.innerHTML = '
Erreur lors de la recherche.
'; + } +} + +function selectTmdbMovie(title, year, poster) { + document.getElementById('f-title').value = title; + document.getElementById('f-year').value = year; + if (poster) { + document.getElementById('f-poster').value = poster; + updatePosterPreview(poster); + } + document.getElementById('search-results-wrap').innerHTML = ''; +} + +function updatePosterPreview(url) { + const img = document.getElementById('poster-preview-img'); + const none = document.getElementById('poster-preview-none'); + const info = document.getElementById('poster-preview-info'); + + if (url) { + img.src = url; + img.style.display = 'block'; + none.style.display = 'none'; + info.innerHTML = "Affiche sélectionnée"; + } else { + img.style.display = 'none'; + none.style.display = 'flex'; + info.innerHTML = "Aucune affiche sélectionnée.
Faites une recherche ou collez une URL."; + } +} + +function openAddModal() { + editingFilmId = null; + document.getElementById('modal-title').textContent = "Nouvelle entrée"; + + document.getElementById('f-type').value = currentTab; + handleTypeChange(currentTab); + + resetStars(); + document.getElementById('f-title').value = ''; + document.getElementById('f-year').value = ''; + document.getElementById('f-director').value = ''; + document.getElementById('f-poster').value = ''; + document.getElementById('f-review').value = ''; + document.getElementById('f-format').value = 'dvd'; + document.getElementById('f-length').value = ''; + document.getElementById('f-publisher').value = ''; + document.getElementById('f-ean').value = ''; + document.getElementById('f-discs').value = '1'; + document.getElementById('f-aspect').value = ''; + document.getElementById('f-description').value = ''; + document.getElementById('search-input').value = ''; + document.getElementById('search-results-wrap').innerHTML = ''; + updatePosterPreview(''); + + document.getElementById('form-overlay').classList.add('open'); +} + +function openEditModal(id) { + const f = films.find(x => x.id === id); + if (!f) return; + + editingFilmId = id; + document.getElementById('modal-title').textContent = "Modifier l'entrée"; + + const type = f.type || 'critique'; + document.getElementById('f-type').value = type; + handleTypeChange(type); + + document.getElementById('f-title').value = f.title || ''; + document.getElementById('f-year').value = f.year || ''; + document.getElementById('f-director').value = f.director || ''; + document.getElementById('f-poster').value = f.poster || ''; + document.getElementById('f-review').value = f.review || ''; + document.getElementById('f-format').value = f.format || 'dvd'; + document.getElementById('f-length').value = f.length || ''; + document.getElementById('f-publisher').value = f.publisher || ''; + document.getElementById('f-ean').value = f.ean_isbn13 || ''; + document.getElementById('f-discs').value = f.number_of_discs || '1'; + document.getElementById('f-aspect').value = f.aspect_ratio || ''; + document.getElementById('f-description').value = f.description || ''; + + document.getElementById('search-input').value = ''; + document.getElementById('search-results-wrap').innerHTML = ''; + + setRating(f.rating || 0); + updatePosterPreview(f.poster || ''); + + document.getElementById('form-overlay').classList.add('open'); +} + +function closeModal() { + document.getElementById('form-overlay').classList.remove('open'); +} + +function saveFilm() { + const title = document.getElementById('f-title').value.trim(); + if (!title) { + alert("Le titre du film est obligatoire !"); + return; + } + const type = document.getElementById('f-type').value; + + const metadataVideotheque = type === 'videotheque' ? { + format: document.getElementById('f-format').value, + length: document.getElementById('f-length').value.trim(), + publisher: document.getElementById('f-publisher').value.trim(), + ean_isbn13: document.getElementById('f-ean').value.trim(), + number_of_discs: document.getElementById('f-discs').value.trim(), + aspect_ratio: document.getElementById('f-aspect').value.trim(), + description: document.getElementById('f-description').value.trim() + } : { + format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '', aspect_ratio: '', description: '' + }; + +if (editingFilmId !== null) { + const index = films.findIndex(f => f.id === editingFilmId); + if (index !== -1) { + films[index] = { + ...films[index], + title: title, + type: type, + year: document.getElementById('f-year').value.trim(), + director: document.getElementById('f-director').value.trim(), + poster: document.getElementById('f-poster').value.trim(), + rating: type === 'critique' ? (currentRating || 1) : 0, + review: type === 'critique' ? document.getElementById('f-review').value.trim() : '', + streaming: films[index].streaming || "Disponible au cinéma ou support physique", // Conserve ou ajoute par défaut + ...metadataVideotheque + }; + } + } else { + const film = { + id: Date.now(), + title: title, + type: type, + year: document.getElementById('f-year').value.trim(), + director: document.getElementById('f-director').value.trim(), + poster: document.getElementById('f-poster').value.trim(), + rating: type === 'critique' ? (currentRating || 1) : 0, + review: type === 'critique' ? document.getElementById('f-review').value.trim() : '', + streaming: "Disponible au cinéma ou support physique", // Valeur par défaut à la création + ...metadataVideotheque + }; + films.unshift(film); + } + persist(); + renderAdminTable(); + closeModal(); +} + +function deleteFilm(id) { + if (confirm("Voulez-vous vraiment supprimer cet élément ?")) { + films = films.filter(f => f.id !== id); + persist(); + renderAdminTable(); + } +} + +function renderAdminTable() { + const tbody = document.getElementById('admin-tbody'); + const emptyState = document.getElementById('admin-empty'); + const emptyMessage = document.getElementById('empty-message'); + + if (!tbody) return; + + // Reset select-all + bulk-delete button BEFORE rebuilding DOM + const selectAll = document.getElementById('th-select-all'); + if (selectAll) selectAll.checked = false; + const btnBulkReset = document.getElementById('btn-bulk-delete'); + if (btnBulkReset) btnBulkReset.style.display = 'none'; + const countSpanReset = document.getElementById('bulk-select-count'); + if (countSpanReset) countSpanReset.textContent = '0'; + + const filteredFilms = films.filter(f => { + const fType = f.type || 'critique'; + return fType === currentTab; + }); + + if (filteredFilms.length === 0) { + tbody.innerHTML = ''; + if (emptyState) { + emptyMessage.textContent = currentTab === 'critique' + ? "Aucun film pour l'instant. Ajoutez votre première critique !" + : "Votre vidéothèque est vide. Ajoutez vos premiers DVD ou Blu-ray !"; + emptyState.style.display = 'block'; + } + return; + } + + if (emptyState) emptyState.style.display = 'none'; + + // Update film count display + const countEl = document.getElementById('films-count'); + if (countEl) countEl.textContent = filteredFilms.length; + + tbody.innerHTML = filteredFilms.map(f => { + const posterHtml = f.poster + ? `` + : '
'; + + let dynamicCellHtml = ''; + if (currentTab === 'critique') { + dynamicCellHtml = `${'★'.repeat(f.rating || 1)}${'☆'.repeat(5 - (f.rating || 1))}`; + } else { + let formatLabel = (f.format || 'DVD').toUpperCase().replace('_4K', ' 4K'); + let publisherLabel = f.publisher ? `
${f.publisher}
` : ''; + dynamicCellHtml = `${formatLabel}${publisherLabel}`; + } + + return ` + + + + + ${posterHtml} + ${f.title} + ${f.year || '—'} + ${f.director || '—'} + ${dynamicCellHtml} + + + + + + `; + }).join(''); +} + +function toggleSelectAll(masterCheckbox) { + const checkboxes = document.querySelectorAll('.row-select'); + checkboxes.forEach(cb => cb.checked = masterCheckbox.checked); + updateBulkDeleteButton(); +} + +function updateBulkDeleteButton() { + const checkedBoxes = document.querySelectorAll('.row-select:checked'); + const btnBulk = document.getElementById('btn-bulk-delete'); + const countSpan = document.getElementById('bulk-select-count'); + + if (btnBulk && countSpan) { + if (checkedBoxes && checkedBoxes.length > 0) { + countSpan.textContent = checkedBoxes.length; + btnBulk.style.display = 'inline-flex'; + } else { + btnBulk.style.display = 'none'; + } + } +} + +function deleteSelectedFilms() { + const checkedBoxes = document.querySelectorAll('.row-select:checked'); + if (!checkedBoxes || checkedBoxes.length === 0) return; + + if (confirm(`Êtes-vous sûr de vouloir supprimer définitivement ces ${checkedBoxes.length} éléments ?`)) { + const idsToDelete = Array.from(checkedBoxes).map(cb => parseInt(cb.getAttribute('data-id'))); + films = films.filter(f => !idsToDelete.includes(f.id)); + persist(); + renderAdminTable(); + } +} + +// ── Letterboxd CSV parser (handles quoted commas correctly) ────────────── +function parseLetterboxdCSV(text) { + const rows = []; + const lines = text.split(/\r\n|\n/); + for (const line of lines) { + if (!line.trim()) continue; + const cols = []; + let cur = '', inQ = false; + for (let i = 0; i < line.length; i++) { + const c = line[i]; + if (c === '"') { + if (inQ && line[i+1] === '"') { cur += '"'; i++; } + else inQ = !inQ; + } else if (c === ',' && !inQ) { + cols.push(cur); cur = ''; + } else { + cur += c; + } + } + cols.push(cur); + rows.push(cols); + } + return rows; +} + +// ── Show import progress overlay ───────────────────────────────────────── +function showImportProgress(total) { + let overlay = document.getElementById('import-progress-overlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'import-progress-overlay'; + overlay.style.cssText = ` + position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:999; + display:flex;align-items:center;justify-content:center; + `; + overlay.innerHTML = ` +
+
+ Import +
+
+ Initialisation… +
+
+
+
+
0 / ${total}
+
+ `; + document.body.appendChild(overlay); + } + return overlay; +} + +function updateImportProgress(done, total, label) { + const bar = document.getElementById('import-progress-bar'); + const lbl = document.getElementById('import-progress-label'); + const cnt = document.getElementById('import-progress-count'); + if (bar) bar.style.width = Math.round((done / total) * 100) + '%'; + if (lbl) lbl.textContent = label; + if (cnt) cnt.textContent = `${done} / ${total}`; +} + +function hideImportProgress() { + const el = document.getElementById('import-progress-overlay'); + if (el) el.remove(); +} + +// ── TMDB poster lookup (single film) ───────────────────────────────────── +async function fetchTmdbPoster(title, year) { + const key = localStorage.getItem('tmdb-api-key'); + if (!key) return ''; + try { + const yearParam = year ? `&year=${year}` : ''; + const res = await fetch( + `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR${yearParam}` + ); + const data = await res.json(); + if (data.results && data.results.length > 0 && data.results[0].poster_path) { + return `https://image.tmdb.org/t/p/w500${data.results[0].poster_path}`; + } + // Retry without year if no result + if (year) { + const res2 = await fetch( + `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR` + ); + const data2 = await res2.json(); + if (data2.results && data2.results.length > 0 && data2.results[0].poster_path) { + return `https://image.tmdb.org/t/p/w500${data2.results[0].poster_path}`; + } + } + } catch (e) {} + return ''; +} + +// ── DVD/Blu-ray cover: Open Library (EAN) → TMDB fallback ──────────────── +async function fetchDvdCover(ean, title, year) { + // Step 1 — Open Library ISBN/EAN lookup (no key needed) + if (ean) { + try { + const olRes = await fetch( + `https://openlibrary.org/api/books?bibkeys=ISBN:${encodeURIComponent(ean)}&format=json&jscmd=data` + ); + const olData = await olRes.json(); + const key = `ISBN:${ean}`; + if (olData[key]) { + const book = olData[key]; + // Prefer large cover, fall back to medium/small + if (book.cover) { + const coverUrl = book.cover.large || book.cover.medium || book.cover.small || ''; + if (coverUrl) return coverUrl; + } + } + } catch (e) {} + + // Step 1b — Open Library cover API directly by ISBN (faster path) + try { + const testUrl = `https://covers.openlibrary.org/b/isbn/${ean}-L.jpg?default=false`; + const testRes = await fetch(testUrl); + if (testRes.ok && testRes.headers.get('content-type')?.includes('image')) { + return testUrl; + } + } catch (e) {} + } + + // Step 2 — TMDB fallback (uses existing key) + return await fetchTmdbPoster(title, year); +} + +// ── Main Letterboxd import (reviews.csv OR ratings.csv) ────────────────── +function importFromCSV(input) { + const file = input.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async function (e) { + const text = e.target.result; + const rows = parseLetterboxdCSV(text); + + if (rows.length <= 1) { + alert("Le fichier semble vide ou invalide."); + input.value = ''; + return; + } + + const headers = rows[0].map(h => h.trim().toLowerCase()); + + // Detect file type: reviews.csv has a "Review" column, ratings.csv doesn't + const isReviews = headers.includes('review'); + const iCol = { // column indices by header name + name: headers.indexOf('name'), + year: headers.indexOf('year'), + rating: headers.indexOf('rating'), + review: isReviews ? headers.indexOf('review') : -1, + watchedDate: headers.indexOf('watched date'), + }; + + // Collect rows to import (skip duplicates first pass) + const toImport = []; + let duplicateCount = 0; + + for (let i = 1; i < rows.length; i++) { + const cols = rows[i]; + const title = iCol.name >= 0 ? cols[iCol.name]?.trim() : ''; + if (!title) continue; + + const year = iCol.year >= 0 ? (cols[iCol.year]?.trim() || '') : ''; + const ratingRaw = iCol.rating >= 0 ? parseFloat(cols[iCol.rating]) : 0; + // Letterboxd uses 0.5–5 scale; convert to our 1–5 integer scale + const rating = Math.round(Math.min(5, Math.max(1, ratingRaw))); + const review = (isReviews && iCol.review >= 0) ? (cols[iCol.review]?.trim() || '') : ''; + + const alreadyExists = films.some( + f => f.title.toLowerCase() === title.toLowerCase() && f.year === year + ); + if (alreadyExists) { duplicateCount++; continue; } + + toImport.push({ title, year, rating, review }); + } + + if (toImport.length === 0) { + alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`); + input.value = ''; + return; + } + + const hasTmdb = !!localStorage.getItem('tmdb-api-key'); + showImportProgress(toImport.length); + + let importedCount = 0; + for (let i = 0; i < toImport.length; i++) { + const { title, year, rating, review } = toImport[i]; + updateImportProgress(i, toImport.length, `🎬 ${title}`); + + let poster = ''; + let streaming = "Disponible au cinéma ou support physique"; + + if (hasTmdb) { + // On récupère les deux infos en un seul traitement + const details = await fetchTmdbDetails(title, year); + poster = details.poster; + streaming = details.streaming; + // Augmentation légère du délai pour respecter les limites de l'API avec le double appel + await new Promise(r => setTimeout(r, 350)); + } + + films.push({ + id: Date.now() + Math.floor(Math.random() * 100000) + i, + title, + type: 'critique', + year, + director: '', + rating, + review, + poster, + streaming, // Ajout du nouveau champ + format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '1', aspect_ratio: '', description: '' + }); + importedCount++; + } + + updateImportProgress(toImport.length, toImport.length, '✅ Terminé !'); + await new Promise(r => setTimeout(r, 600)); + hideImportProgress(); + + films.sort((a, b) => b.id - a.id); + persist(); + renderAdminTable(); + + const typeLabel = isReviews ? 'reviews.csv (avec critiques)' : 'ratings.csv (notes seules)'; + const posterInfo = hasTmdb ? ' · Affiches récupérées via TMDB' : ' · ⚠️ Clé TMDB absente, affiches non récupérées'; + alert( + `Import Letterboxd — ${typeLabel}\n` + + `✅ ${importedCount} film(s) importé(s)${posterInfo}\n` + + `⚠️ ${duplicateCount} doublon(s) ignoré(s).` + ); + input.value = ''; + }; + reader.readAsText(file, 'UTF-8'); +} + +function exportToCSV() { + if (films.length === 0) { alert("Aucune donnée à exporter."); return; } + let csvContent = "ID;Titre;Annee;Realisateur;Note;Critique;URL_Affiche;Type;Format\r\n"; + films.filter(f => (f.type || 'critique') === 'critique').forEach(f => { + const cleanTitle = (f.title || '').replace(/"/g, '""'); + const cleanDirector = (f.director || '').replace(/"/g, '""'); + const cleanReview = (f.review || '').replace(/\r?\n|\r/g, ' ').replace(/"/g, '""'); + const cleanPoster = (f.poster || '').replace(/"/g, '""'); + csvContent += `${f.id};"${cleanTitle}";${f.year || ''};"${cleanDirector}";${f.rating || 0};"${cleanReview}";"${cleanPoster}";"critique";""\r\n`; + }); + const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", `export_critiques_${Date.now()}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +function importVideothequeFromInput(input) { + const file = input.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async function(e) { + await importVideothequeCSV(e.target.result); + input.value = ''; + }; + reader.readAsText(file, 'UTF-8'); +} + +async function importVideothequeCSV(text) { + // ── Parse CSV (handles quoted commas) ── + const lines = []; + let row = [""]; + let inQuotes = false; + for (let i = 0; i < text.length; i++) { + let c = text[i], next = text[i+1]; + if (c === '"') { + if (inQuotes && next === '"') { row[row.length - 1] += '"'; i++; } + else { inQuotes = !inQuotes; } + } else if (c === ',' && !inQuotes) { + row.push(''); + } else if ((c === '\r' || c === '\n') && !inQuotes) { + if (c === '\r' && next === '\n') { i++; } + lines.push(row); row = ['']; + } else { + row[row.length - 1] += c; + } + } + if (row.length > 1 || row[0] !== '') lines.push(row); + if (lines.length <= 1) return; + + // ── Collect rows to import ── + const toImport = []; + let duplicateCount = 0; + + for (let i = 1; i < lines.length; i++) { + const data = lines[i]; + if (data.length < 2 || !data[1]) continue; + + const title = data[1].trim(); + const creators = data[2] ? data[2].trim() : ''; + const ean = data[6] ? data[6].trim() : ''; + const description = data[8] ? data[8].trim() : ''; + const publisher = data[9] ? data[9].trim() : ''; + const publishDate = data[10] ? data[10].trim() : ''; + const length = data[15] ? data[15].trim() : ''; + const discs = data[16] ? data[16].trim() : '1'; + const aspect = data[20] ? data[20].trim() : ''; + + let year = publishDate ? publishDate.split('-')[0] : ''; + + const lowerTitle = title.toLowerCase(); + const lowerDesc = description.toLowerCase(); + let format = 'dvd'; + if (lowerTitle.includes('blu-ray') || lowerTitle.includes('bluray') || + lowerDesc.includes('blu-ray') || lowerTitle.includes('4k')) { + format = (lowerTitle.includes('4k') || lowerDesc.includes('4k')) ? 'bluray_4k' : 'bluray'; + } + + const duplicate = films.some( + f => f.type === 'videotheque' && + f.title.toLowerCase() === title.toLowerCase() && + f.year === year + ); + if (duplicate) { duplicateCount++; continue; } + + toImport.push({ title, creators, ean, description, publisher, year, length, discs, aspect, format }); + } + + if (toImport.length === 0) { + alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`); + return; + } + + // ── Progress bar ── + showImportProgress(toImport.length); + let importedCount = 0; + let coversFound = 0; + + for (let i = 0; i < toImport.length; i++) { + const { title, creators, ean, description, publisher, year, length, discs, aspect, format } = toImport[i]; + + const sourceLabel = ean ? `EAN ${ean}` : title; + updateImportProgress(i, toImport.length, `📀 ${sourceLabel}`); + + // ── Fetch cover: Open Library (EAN) → TMDB ── + let poster = ''; + try { + poster = await fetchDvdCover(ean, title, year); + if (poster) coversFound++; + } catch(e) {} + + // Respectful delay + await new Promise(r => setTimeout(r, 300)); + + films.push({ + id: Date.now() + Math.floor(Math.random() * 100000) + i, + title, type: 'videotheque', + director: creators, year, poster, format, length, + publisher, ean_isbn13: ean, + number_of_discs: discs, + aspect_ratio: aspect, + description, rating: 0, review: '' + }); + importedCount++; + } + + updateImportProgress(toImport.length, toImport.length, '✅ Terminé !'); + await new Promise(r => setTimeout(r, 600)); + hideImportProgress(); + + persist(); + renderAdminTable(); + alert( + `Import Vidéothèque\n` + + `✅ ${importedCount} film(s) importé(s)\n` + + `🖼️ ${coversFound} jaquette(s) trouvée(s) (Open Library + TMDB)\n` + + `⚠️ ${duplicateCount} doublon(s) ignoré(s).` + ); +} + +function exportVideothequeToCSV() { + const filmsVideo = films.filter(f => (f.type || 'critique') === 'videotheque'); + if (filmsVideo.length === 0) { alert("Aucune donnée de vidéothèque à exporter."); return; } + + let csvContent = "item_type,title,creators,ean_isbn13,description,publisher,publish_date,length,number_of_discs,aspect_ratio\r\n"; + filmsVideo.forEach(f => { + const cleanTitle = (f.title || '').replace(/"/g, '""'); + const cleanDirector = (f.director || '').replace(/"/g, '""'); + const cleanEan = (f.ean_isbn13 || ''); + const cleanDesc = (f.description || '').replace(/\r?\n|\r/g, ' ').replace(/"/g, '""'); + const cleanPublisher = (f.publisher || '').replace(/"/g, '""'); + const fakePublishDate = f.year ? `${f.year}-01-01` : ''; + csvContent += `movie,"${cleanTitle}","${cleanDirector}","${cleanEan}","${cleanDesc}","${cleanPublisher}","${fakePublishDate}","${f.length || ''}","${f.number_of_discs || '1'}","${f.aspect_ratio || ''}"\r\n`; + }); + + const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", `export_videotheque_${Date.now()}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +function goPublic() { window.location.href = '../index.html'; } + +document.addEventListener('DOMContentLoaded', () => { + const savedKey = localStorage.getItem('tmdb-api-key'); + if (savedKey && document.getElementById('tmdb-key-input')) { + document.getElementById('tmdb-key-input').value = savedKey; + } + renderAdminTable(); +}); + +// Récupère l'affiche ET les plateformes de streaming en France +async function fetchTmdbDetails(title, year) { + const key = localStorage.getItem('tmdb-api-key'); + let streamingDefault = "Disponible au cinéma ou support physique"; + if (!key) return { poster: '', streaming: streamingDefault }; + + try { + const yearParam = year ? `&year=${year}` : ''; + const res = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR${yearParam}`); + const data = await res.json(); + + let movie = data.results && data.results.length > 0 ? data.results[0] : null; + + if (!movie && year) { + const res2 = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR`); + const data2 = await res2.json(); + if (data2.results && data2.results.length > 0) movie = data2.results[0]; + } + + if (movie) { + const poster = movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : ''; + let streaming = streamingDefault; + + try { + const providerRes = await fetch(`https://api.themoviedb.org/3/movie/${movie.id}/watch/providers?api_key=${key}`); + const providerData = await providerRes.json(); + + // On cherche les plateformes d'abonnement (flatrate) disponibles en France (FR) + if (providerData.results && providerData.results.FR && providerData.results.FR.flatrate) { + const providers = providerData.results.FR.flatrate.map(p => p.provider_name); + if (providers.length > 0) { + streaming = providers.join(', '); + } + } + } catch (e) { + console.error("Erreur fournisseurs streaming:", e); + } + return { poster, streaming }; + } + } catch (e) { + console.error("Erreur recherche TMDB:", e); + } + return { poster: '', streaming: streamingDefault }; +} \ No newline at end of file diff --git a/js/public.js b/js/public.js new file mode 100644 index 0000000..fede958 --- /dev/null +++ b/js/public.js @@ -0,0 +1,265 @@ +const STORAGE_KEY = 'mon-cinema-films'; +let films = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); +let currentPubTab = 'critique'; +let activeRatingFilter = 0; // 0 = tous + +function switchPubTab(tabName) { + currentPubTab = tabName; + activeRatingFilter = 0; + document.querySelectorAll('.rating-filter-btn').forEach(btn => { + btn.classList.remove('active'); + btn.querySelectorAll('.rf-star').forEach(s => s.classList.remove('filled')); + }); + + const tabCritiques = document.getElementById('tab-pub-critiques'); + const tabVideotheque = document.getElementById('tab-pub-videotheque'); + + if (tabCritiques && tabVideotheque) { + if (tabName === 'critique') { + tabCritiques.classList.add('active'); + tabVideotheque.classList.remove('active'); + } else { + tabVideotheque.classList.add('active'); + tabCritiques.classList.remove('active'); + } + } + + renderPublicGrid(); +} + +function filterByRating(stars) { + activeRatingFilter = (activeRatingFilter === stars) ? 0 : stars; + + // Update button states + document.querySelectorAll('.rating-filter-btn').forEach(btn => { + const n = parseInt(btn.dataset.stars); + btn.classList.toggle('active', n === activeRatingFilter); + // Highlight filled stars up to active filter + btn.querySelectorAll('.rf-star').forEach((s, i) => { + s.classList.toggle('filled', activeRatingFilter > 0 && i < activeRatingFilter); + }); + }); + + renderPublicGrid(); +} + +function renderPublicGrid() { + const grid = document.getElementById('grid'); + const emptyState = document.getElementById('empty-state'); + const countLabel = document.getElementById('count-label'); + + if (!grid) return; + + // Show rating filter only on critique tab + const filterBar = document.getElementById('rating-filter-bar'); + if (filterBar) filterBar.style.display = currentPubTab === 'critique' ? 'flex' : 'none'; + + const filtered = films.filter(f => { + const fType = f.type || 'critique'; + if (fType !== currentPubTab) return false; + if (currentPubTab === 'critique' && activeRatingFilter > 0) { + return (f.rating || f.note || 1) === activeRatingFilter; + } + return true; + }); + + if (countLabel) { + if (currentPubTab === 'critique') { + const suffix = activeRatingFilter > 0 ? ` · filtrées ${activeRatingFilter}★` : ''; + countLabel.textContent = filtered.length + (filtered.length > 1 ? ' critiques' : ' critique') + suffix; + } else { + countLabel.textContent = filtered.length + (filtered.length > 1 ? ' films physiques' : ' film physique'); + } + } + + if (filtered.length === 0) { + grid.innerHTML = ''; + if (emptyState) { + emptyState.style.display = 'block'; + emptyState.querySelector('p').textContent = currentPubTab === 'critique' + ? "Aucune critique pour l'instant." + : "Aucun film dans la vidéothèque pour l'instant."; + } + return; + } + + if (emptyState) emptyState.style.display = 'none'; + + grid.innerHTML = filtered.map(f => { + const posterUrl = f.poster || f.image || f.affiche || ''; + const movieTitle = f.title || f.titre || 'Sans titre'; + const movieYear = f.year || f.annee || f.publish_date || '—'; + const movieDirector = f.director || f.creators || f.realisateur || '—'; + + const posterHtml = posterUrl + ? `Affiche ${movieTitle}` + : `
+ + ${movieTitle} +
`; + + let footerInfoHtml = ''; + if (currentPubTab === 'critique') { + const rating = f.rating || f.note || 1; + footerInfoHtml = `
${"★".repeat(rating)}${"☆".repeat(5 - rating)}
`; + } else { + const format = (f.format || f.support || 'DVD').toUpperCase().replace('_4K', ' 4K'); + const length = f.length || f.duree || ''; + footerInfoHtml = ` + `; + } + + return ` +
+
+ ${posterHtml} +
+
+

${movieTitle}

+
${movieYear.toString().substring(0,4)} · ${movieDirector}
+ ${footerInfoHtml} +
+
+ `; + }).join(''); +} + +function openDetail(id) { + const f = films.find(x => x.id === id); + if (!f) return; + + const detailOverlay = document.getElementById('detail-overlay'); + const modalLayout = document.getElementById('detail-modal-layout'); + const dPoster = document.getElementById('d-poster'); + const dPosterWrap = document.getElementById('d-poster-wrap'); + const dTitle = document.getElementById('d-title'); + const dMeta = document.getElementById('d-meta'); + const dStars = document.getElementById('d-stars'); + const dReview = document.getElementById('d-review'); + + const movieTitle = f.title || f.titre || 'Sans titre'; + const movieYear = f.year || f.annee || f.publish_date || '—'; + const movieDirector = f.director || f.creators || f.realisateur || '—'; + const posterUrl = f.poster || f.image || f.affiche || ''; + + if (dTitle) dTitle.textContent = movieTitle; + if (dMeta) dMeta.textContent = [movieYear.toString().substring(0,4), movieDirector].filter(Boolean).join(' · '); + + const fType = f.type || 'critique'; + + // Gestion de l'affichage de la colonne image + if (posterUrl) { + if (dPoster) { dPoster.src = posterUrl; dPoster.style.display = 'block'; } + if (dPosterWrap) { + dPosterWrap.style.display = 'block'; + dPosterWrap.classList.remove('poster-critique', 'poster-video'); + dPosterWrap.classList.add(fType === 'critique' ? 'poster-critique' : 'poster-video'); + } + if (modalLayout) modalLayout.classList.remove('no-poster'); + } else { + if (dPoster) dPoster.style.display = 'none'; + if (dPosterWrap) dPosterWrap.style.display = 'none'; + if (modalLayout) modalLayout.classList.add('no-poster'); + } + + // Préparation du badge de visionnage/streaming (commun aux deux types de fiches) + const streamingInfo = f.streaming || "Disponible au cinéma ou support physique"; + const isPlatform = streamingInfo !== "Disponible au cinéma ou support physique"; + const streamIcon = isPlatform ? "ti-device-tv" : "ti-movie"; + + const streamingBadgeHtml = ` +
+ + ${streamingInfo} +
+ `; + + if (fType === 'critique') { + // ---------------------------------------- + // POP-UP MODE : CRITIQUE JOURNAL + // ---------------------------------------- + if (dStars) { + dStars.style.display = 'block'; + const rating = f.rating || f.note || 1; + dStars.innerHTML = "★".repeat(rating) + `${"☆".repeat(5 - rating)}`; + } + + if (dReview) { + const reviewText = f.review || f.critique || "Aucune critique rédigée pour ce film."; + + dReview.innerHTML = ` + ${streamingBadgeHtml} + +
+
+

${reviewText}

+
+ `; + } + } else { + // ---------------------------------------- + // POP-UP MODE : VIDEOTHEQUE PREMIUM + // ---------------------------------------- + if (dStars) dStars.style.display = 'none'; + + const format = (f.format || f.support || 'DVD').toUpperCase().replace('_4K', ' 4K'); + const publisher = f.publisher || f.editeur || ''; + const length = f.length || f.duree || ''; + const discs = f.number_of_discs || f.disques || f.nb_disques || ''; + const ratio = f.aspect_ratio || f.format_image || ''; + const ean = f.ean_isbn13 || f.ean || ''; + const description = f.description || f.synopsis || "Aucun synopsis disponible dans le catalogue."; + + if (dReview) { + dReview.innerHTML = ` + ${streamingBadgeHtml} + +
+ ${format} + ${length ? ` ${length} Min` : ''} + ${discs ? ` ${discs} ${discs > 1 ? 'Disques' : 'Disque'}` : ''} +
+ +
+ ${publisher ? ` +
+ Éditeur / Studio + ${publisher} +
` : ''} + + ${ratio ? ` +
+ Format d'image + ${ratio} +
` : ''} + + ${ean ? ` +
+ Code-barres (EAN) + ${ean} +
` : ''} +
+ +
+

Synopsis

+

${description}

+
+ `; + } + } + + if (detailOverlay) detailOverlay.classList.add('open'); +} +function closeDetail() { + const detailOverlay = document.getElementById('detail-overlay'); + if (detailOverlay) { + detailOverlay.classList.remove('open'); + } +} + +document.addEventListener('DOMContentLoaded', () => { + renderPublicGrid(); +}); \ No newline at end of file