Init PercoVitrine — app de gestion de fiches produits
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
@@ -0,0 +1,10 @@
|
||||
Options -Indexes
|
||||
RewriteEngine On
|
||||
|
||||
# Bloquer accès au dossier data
|
||||
RewriteRule ^data/ - [F,L]
|
||||
|
||||
# Router tout vers le bon fichier PHP
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
FROM php:8.3-apache
|
||||
|
||||
RUN docker-php-ext-install pdo pdo_sqlite && \
|
||||
a2enmod rewrite
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html && \
|
||||
echo "ServerName localhost" >> /etc/apache2/apache2.conf
|
||||
|
||||
COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require __DIR__ . '/includes/helpers.php';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
/* ─── DELETE product ─────────────────────────────────────── */
|
||||
if ($action === 'product' && $method === 'DELETE') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) err('id manquant');
|
||||
$medias = $pdo->prepare("SELECT filename FROM product_media WHERE product_id=?")->execute([$id]) ? $pdo->query("SELECT filename FROM product_media WHERE product_id=$id")->fetchAll() : [];
|
||||
foreach ($medias as $m) { if ($m['filename']) @unlink($UPLOAD_DIR . $m['filename']); }
|
||||
$pdo->prepare("DELETE FROM products WHERE id=?")->execute([$id]);
|
||||
ok(null);
|
||||
}
|
||||
|
||||
/* ─── DELETE media ───────────────────────────────────────── */
|
||||
if ($action === 'media' && $method === 'DELETE') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) err('id manquant');
|
||||
$m = $pdo->prepare("SELECT filename FROM product_media WHERE id=?")->execute([$id])
|
||||
? $pdo->query("SELECT filename FROM product_media WHERE id=$id")->fetch()
|
||||
: null;
|
||||
if ($m && $m['filename']) @unlink($UPLOAD_DIR . $m['filename']);
|
||||
$pdo->prepare("DELETE FROM product_media WHERE id=?")->execute([$id]);
|
||||
ok(null);
|
||||
}
|
||||
|
||||
/* ─── PATCH media sort ───────────────────────────────────── */
|
||||
if ($action === 'media_sort' && $method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$ids = $body['ids'] ?? [];
|
||||
$stmt = $pdo->prepare("UPDATE product_media SET sort_order=? WHERE id=?");
|
||||
foreach ($ids as $i => $id) $stmt->execute([$i, (int)$id]);
|
||||
ok(null);
|
||||
}
|
||||
|
||||
/* ─── GET product JSON ───────────────────────────────────── */
|
||||
if ($action === 'product' && $method === 'GET') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) err('id manquant');
|
||||
$p = $pdo->prepare("SELECT * FROM products WHERE id=?")->execute([$id])
|
||||
? $pdo->query("SELECT * FROM products WHERE id=$id")->fetch()
|
||||
: null;
|
||||
if (!$p) err('introuvable', 404);
|
||||
$p['media'] = $pdo->query("SELECT * FROM product_media WHERE product_id={$id} ORDER BY sort_order")->fetchAll();
|
||||
ok($p);
|
||||
}
|
||||
|
||||
/* ─── POST upload media ──────────────────────────────────── */
|
||||
if ($action === 'upload' && $method === 'POST') {
|
||||
$pid = (int)($_POST['product_id'] ?? 0);
|
||||
if (!$pid) err('product_id manquant');
|
||||
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
|
||||
$orig = $_FILES['file']['name'];
|
||||
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
||||
$allowed = array_merge(imgExts(), vidExts());
|
||||
if (!in_array($ext, $allowed)) err('format non supporté');
|
||||
$fname = uniqid('m', true) . '.' . $ext;
|
||||
if (!move_uploaded_file($_FILES['file']['tmp_name'], $UPLOAD_DIR . $fname)) err('échec upload');
|
||||
$type = isImage($fname) ? 'image' : 'video';
|
||||
$order = (int)$pdo->query("SELECT COALESCE(MAX(sort_order),0)+1 FROM product_media WHERE product_id=$pid")->fetchColumn();
|
||||
$pdo->prepare("INSERT INTO product_media (product_id,type,filename,original_name,sort_order) VALUES(?,?,?,?,?)")
|
||||
->execute([$pid, $type, $fname, $orig, $order]);
|
||||
$newId = $pdo->lastInsertId();
|
||||
ok(['id'=>$newId,'type'=>$type,'filename'=>$fname,'original_name'=>$orig,'sort_order'=>$order]);
|
||||
}
|
||||
// Video URL
|
||||
if (!empty($_POST['video_url'])) {
|
||||
$url = filter_var($_POST['video_url'], FILTER_SANITIZE_URL);
|
||||
$order = (int)$pdo->query("SELECT COALESCE(MAX(sort_order),0)+1 FROM product_media WHERE product_id=$pid")->fetchColumn();
|
||||
$pdo->prepare("INSERT INTO product_media (product_id,type,video_url,sort_order) VALUES(?,?,?,?)")
|
||||
->execute([$pid, 'video', $url, $order]);
|
||||
ok(['id'=>$pdo->lastInsertId(),'type'=>'video','video_url'=>$url,'sort_order'=>$order]);
|
||||
}
|
||||
err('aucun fichier');
|
||||
}
|
||||
|
||||
err('action inconnue', 404);
|
||||
@@ -0,0 +1,143 @@
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#0d1117;--bg-card:#161b22;--bg-input:#0d1321;
|
||||
--border:#30363d;--text:#f1f5f9;--muted:#8b949e;
|
||||
--primary:#2DA3F2;--primary-dark:#1a8dd9;
|
||||
--green:#22c55e;--red:#ef4444;--volt:#facc15;
|
||||
--shadow:0 4px 24px rgba(0,0,0,.4);
|
||||
}
|
||||
body{font-family:"Outfit",system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;font-size:15px}
|
||||
|
||||
/* Layout */
|
||||
.app-wrap{display:flex;min-height:100vh}
|
||||
.sidebar{width:220px;background:var(--bg-card);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100;overflow-y:auto}
|
||||
.sidebar-brand{padding:1.25rem 1rem 1rem;font-size:1.1rem;font-weight:800;background:linear-gradient(135deg,var(--primary),var(--volt));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;border-bottom:1px solid var(--border);text-decoration:none;display:block}
|
||||
.sidebar-nav{flex:1;padding:.5rem}
|
||||
.sidebar-link{display:flex;align-items:center;gap:.6rem;padding:.5rem .75rem;color:var(--muted);text-decoration:none;border-radius:8px;font-size:.875rem;font-weight:500;margin:.1rem 0;transition:all .2s}
|
||||
.sidebar-link i{width:16px;text-align:center;font-size:.8rem}
|
||||
.sidebar-link:hover{background:rgba(45,163,242,.08);color:var(--primary)}
|
||||
.sidebar-link.active{background:rgba(45,163,242,.14);color:var(--primary);border:1px solid rgba(45,163,242,.2)}
|
||||
.sidebar-section{font-size:.62rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);padding:.75rem .75rem .25rem;font-weight:700}
|
||||
.main{margin-left:220px;flex:1;padding:2rem}
|
||||
|
||||
/* Cards */
|
||||
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:1.5rem}
|
||||
.page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.75rem;flex-wrap:wrap;gap:.75rem}
|
||||
.page-title{font-size:1.4rem;font-weight:800}
|
||||
.page-sub{font-size:.82rem;color:var(--muted);margin-top:.2rem}
|
||||
|
||||
/* Badges */
|
||||
.badge{display:inline-flex;align-items:center;padding:.2rem .65rem;border-radius:20px;font-size:.72rem;font-weight:700;gap:.3rem}
|
||||
.badge-muted{background:rgba(139,148,158,.1);color:var(--muted);border:1px solid rgba(139,148,158,.2)}
|
||||
.badge-volt{background:rgba(250,204,21,.1);color:var(--volt);border:1px solid rgba(250,204,21,.2)}
|
||||
.badge-green{background:rgba(34,197,94,.1);color:var(--green);border:1px solid rgba(34,197,94,.2)}
|
||||
.badge-red{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.2)}
|
||||
.badge-blue{background:rgba(45,163,242,.1);color:var(--primary);border:1px solid rgba(45,163,242,.2)}
|
||||
|
||||
/* Buttons */
|
||||
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.45rem 1rem;border-radius:8px;font-size:.875rem;font-weight:600;cursor:pointer;border:none;text-decoration:none;transition:all .2s;font-family:inherit}
|
||||
.btn-primary{background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff}
|
||||
.btn-primary:hover{opacity:.9;box-shadow:0 0 20px rgba(45,163,242,.3)}
|
||||
.btn-secondary{background:transparent;color:var(--muted);border:1px solid var(--border)}
|
||||
.btn-secondary:hover{color:var(--text);border-color:var(--primary)}
|
||||
.btn-danger{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.25)}
|
||||
.btn-danger:hover{background:rgba(239,68,68,.2)}
|
||||
.btn-success{background:rgba(34,197,94,.12);color:var(--green);border:1px solid rgba(34,197,94,.25)}
|
||||
.btn-success:hover{background:rgba(34,197,94,.22)}
|
||||
.btn-sm{padding:.3rem .7rem;font-size:.8rem}
|
||||
.btn-icon{width:34px;height:34px;padding:0;justify-content:center;border-radius:8px}
|
||||
|
||||
/* Forms */
|
||||
.form-group{margin-bottom:1.1rem}
|
||||
label{display:block;font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem}
|
||||
input,select,textarea{background:var(--bg-input);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:.5rem .75rem;font-family:inherit;font-size:.875rem;width:100%;transition:border-color .2s,box-shadow .2s}
|
||||
input:focus,select:focus,textarea:focus{border-color:var(--primary);outline:none;box-shadow:0 0 0 3px rgba(45,163,242,.1)}
|
||||
input::placeholder,textarea::placeholder{color:var(--muted)}
|
||||
select option{background:var(--bg-card)}
|
||||
textarea{resize:vertical;min-height:140px;line-height:1.6}
|
||||
.input-group{display:flex;gap:.5rem;align-items:center}
|
||||
.input-prefix{background:var(--bg-card);border:1px solid var(--border);border-right:none;color:var(--muted);padding:.5rem .75rem;border-radius:8px 0 0 8px;font-size:.875rem;white-space:nowrap}
|
||||
.input-group input{border-radius:0 8px 8px 0}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.form-row-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}
|
||||
.form-hint{font-size:.74rem;color:var(--muted);margin-top:.3rem}
|
||||
|
||||
/* Product grid */
|
||||
.products-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem;margin-top:1rem}
|
||||
.product-card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;overflow:hidden;text-decoration:none;color:inherit;display:flex;flex-direction:column;transition:border-color .2s,box-shadow .2s,transform .15s}
|
||||
.product-card:hover{border-color:rgba(45,163,242,.3);box-shadow:0 4px 24px rgba(45,163,242,.1);transform:translateY(-2px)}
|
||||
.product-thumb{width:100%;aspect-ratio:1;object-fit:cover;background:var(--bg);display:block}
|
||||
.product-thumb-placeholder{width:100%;aspect-ratio:1;background:var(--bg);display:flex;align-items:center;justify-content:center;color:var(--border);font-size:3rem}
|
||||
.product-body{padding:1rem;flex:1;display:flex;flex-direction:column;gap:.4rem}
|
||||
.product-title{font-weight:700;font-size:.95rem;color:var(--text)}
|
||||
.product-meta{display:flex;justify-content:space-between;align-items:center;margin-top:auto;padding-top:.5rem}
|
||||
.product-price{font-size:1rem;font-weight:800;color:var(--primary)}
|
||||
.product-sku{font-size:.72rem;color:var(--muted)}
|
||||
.product-cat{font-size:.72rem;color:var(--muted)}
|
||||
.product-actions{display:flex;gap:.4rem;padding:.75rem 1rem;border-top:1px solid var(--border);background:rgba(0,0,0,.15)}
|
||||
|
||||
/* Media gallery */
|
||||
.media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:.6rem;margin-top:.6rem}
|
||||
.media-item{position:relative;border-radius:8px;overflow:hidden;aspect-ratio:1;background:var(--bg);border:1px solid var(--border)}
|
||||
.media-item img,.media-item video{width:100%;height:100%;object-fit:cover}
|
||||
.media-item .del-btn{position:absolute;top:4px;right:4px;background:rgba(0,0,0,.7);color:#fff;border:none;border-radius:50%;width:22px;height:22px;font-size:.7rem;cursor:pointer;display:none;align-items:center;justify-content:center}
|
||||
.media-item:hover .del-btn{display:flex}
|
||||
.media-item .sort-handle{position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.7);color:#fff;border-radius:4px;padding:2px 5px;font-size:.65rem;cursor:grab}
|
||||
.media-item.video-item::after{content:'▶';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:1.5rem;color:#fff;pointer-events:none;text-shadow:0 0 8px rgba(0,0,0,.8)}
|
||||
.drop-zone{border:2px dashed var(--border);border-radius:10px;padding:2rem;text-align:center;color:var(--muted);cursor:pointer;transition:all .2s;position:relative}
|
||||
.drop-zone.drag-over{border-color:var(--primary);background:rgba(45,163,242,.05);color:var(--primary)}
|
||||
.drop-zone input[type=file]{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
|
||||
.preview-strip{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.6rem}
|
||||
.preview-item{width:72px;height:72px;border-radius:6px;overflow:hidden;border:1px solid var(--border);position:relative;flex-shrink:0}
|
||||
.preview-item img,.preview-item video{width:100%;height:100%;object-fit:cover}
|
||||
.preview-item .rm{position:absolute;top:-5px;right:-5px;background:var(--red);color:#fff;border:none;border-radius:50%;width:16px;height:16px;font-size:.6rem;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
||||
|
||||
/* Detail view */
|
||||
.product-detail-grid{display:grid;grid-template-columns:1fr 340px;gap:1.5rem;align-items:start}
|
||||
.media-main img,.media-main video{width:100%;border-radius:10px;aspect-ratio:1;object-fit:cover}
|
||||
.media-thumbs{display:flex;gap:.5rem;margin-top:.6rem;flex-wrap:wrap}
|
||||
.media-thumb{width:64px;height:64px;border-radius:6px;overflow:hidden;border:2px solid transparent;cursor:pointer;flex-shrink:0}
|
||||
.media-thumb.active{border-color:var(--primary)}
|
||||
.media-thumb img,.media-thumb video{width:100%;height:100%;object-fit:cover}
|
||||
.product-info-card{display:flex;flex-direction:column;gap:1rem}
|
||||
.price-block{font-size:2rem;font-weight:800;color:var(--primary)}
|
||||
.shipping-block{font-size:.85rem;color:var(--muted)}
|
||||
.info-row{display:flex;justify-content:space-between;padding:.6rem 0;border-bottom:1px solid var(--border);font-size:.875rem}
|
||||
.info-row:last-child{border:none}
|
||||
.info-label{color:var(--muted);font-weight:600}
|
||||
.desc-block{white-space:pre-wrap;line-height:1.7;font-size:.9rem;color:var(--text)}
|
||||
|
||||
/* Etsy panel */
|
||||
.etsy-panel{background:linear-gradient(135deg,rgba(241,90,34,.05),rgba(241,90,34,.02));border:1px solid rgba(241,90,34,.2);border-radius:12px;padding:1.25rem}
|
||||
.etsy-logo{font-size:1.1rem;font-weight:800;color:#f15a22}
|
||||
.etsy-status-ok{color:var(--green);font-size:.85rem}
|
||||
.etsy-status-ko{color:var(--muted);font-size:.85rem}
|
||||
|
||||
/* Filters */
|
||||
.filters{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center}
|
||||
.filter-chip{padding:.3rem .75rem;border-radius:20px;font-size:.78rem;font-weight:600;cursor:pointer;border:1px solid var(--border);background:transparent;color:var(--muted);text-decoration:none;transition:all .2s}
|
||||
.filter-chip:hover,.filter-chip.active{background:rgba(45,163,242,.12);color:var(--primary);border-color:rgba(45,163,242,.3)}
|
||||
|
||||
/* Table */
|
||||
.table{width:100%;border-collapse:collapse}
|
||||
.table th{text-align:left;padding:.6rem .75rem;font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);border-bottom:1px solid var(--border)}
|
||||
.table td{padding:.75rem;border-bottom:1px solid rgba(48,54,61,.5);font-size:.875rem}
|
||||
.table tr:last-child td{border:none}
|
||||
.table tr:hover td{background:rgba(255,255,255,.02)}
|
||||
|
||||
/* Utils */
|
||||
.flex{display:flex}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}
|
||||
.items-center{align-items:center}.justify-between{justify-content:space-between}
|
||||
.flex-wrap{flex-wrap:wrap}.flex-1{flex:1}.flex-col{flex-direction:column}
|
||||
.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}
|
||||
.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}
|
||||
.text-muted{color:var(--muted)}.text-sm{font-size:.875rem}.text-xs{font-size:.75rem}
|
||||
.font-bold{font-weight:700}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.empty-state{text-align:center;padding:4rem 2rem;color:var(--muted)}
|
||||
.empty-state i{font-size:3rem;opacity:.2;margin-bottom:1rem;display:block}
|
||||
.toast-wrap{position:fixed;bottom:1.5rem;right:1.5rem;z-index:9999;display:flex;flex-direction:column;gap:.5rem}
|
||||
.toast{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:.7rem 1rem;font-size:.875rem;min-width:200px;color:var(--text);box-shadow:var(--shadow);border-left:4px solid var(--primary);animation:slideIn .2s ease}
|
||||
.toast.error{border-left-color:var(--red)}
|
||||
@keyframes slideIn{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}
|
||||
.sep{border:none;border-top:1px solid var(--border);margin:1.25rem 0}
|
||||
@media(max-width:768px){.sidebar{width:56px}.sidebar .sidebar-brand span,.sidebar .sidebar-section,.sidebar .sidebar-link span{display:none}.main{margin-left:56px;padding:1rem}.form-row,.form-row-3{grid-template-columns:1fr}.product-detail-grid{grid-template-columns:1fr}}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require __DIR__ . '/includes/helpers.php';
|
||||
require __DIR__ . '/includes/layout.php';
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['_action'] ?? '';
|
||||
if ($action === 'add') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
if (!$name) { $error = 'Le nom est obligatoire.'; }
|
||||
else {
|
||||
try { $pdo->prepare("INSERT INTO categories (name) VALUES(?)")->execute([$name]); }
|
||||
catch (PDOException) { $error = 'Ce nom existe déjà.'; }
|
||||
}
|
||||
} elseif ($action === 'rename') {
|
||||
$cid = (int)($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
if ($cid && $name) {
|
||||
try { $pdo->prepare("UPDATE categories SET name=? WHERE id=?")->execute([$name,$cid]); }
|
||||
catch (PDOException) { $error = 'Ce nom existe déjà.'; }
|
||||
}
|
||||
} elseif ($action === 'delete') {
|
||||
$cid = (int)($_POST['id'] ?? 0);
|
||||
if ($cid) {
|
||||
$pdo->prepare("UPDATE products SET category_id=NULL WHERE category_id=?")->execute([$cid]);
|
||||
$pdo->prepare("DELETE FROM categories WHERE id=?")->execute([$cid]);
|
||||
}
|
||||
}
|
||||
if (!$error) { header('Location: /categories.php'); exit; }
|
||||
}
|
||||
|
||||
$cats = $pdo->query("SELECT c.*, COUNT(p.id) as nb FROM categories c LEFT JOIN products p ON p.category_id=c.id GROUP BY c.id ORDER BY c.name")->fetchAll();
|
||||
|
||||
layout_head('Catégories');
|
||||
layout_sidebar('categories');
|
||||
layout_main_open();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-title"><i class="fas fa-tags" style="color:var(--primary);margin-right:.4rem"></i>Catégories</div>
|
||||
<button onclick="document.getElementById('addModal').style.display='flex'" class="btn btn-primary"><i class="fas fa-plus"></i> Ajouter</button>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?><div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;color:var(--red)"><?= h($error) ?></div><?php endif ?>
|
||||
|
||||
<?php if (!$cats): ?>
|
||||
<div class="empty-state"><i class="fas fa-tags"></i><p>Aucune catégorie.<br>Crée-en une pour organiser tes produits.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="card" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>Nom</th><th style="width:100px;text-align:center">Produits</th><th style="width:120px"></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<tr>
|
||||
<td><strong><?= h($c['name']) ?></strong></td>
|
||||
<td style="text-align:center"><span class="badge badge-muted"><?= $c['nb'] ?></span></td>
|
||||
<td style="text-align:right">
|
||||
<div style="display:flex;gap:.4rem;justify-content:flex-end">
|
||||
<button onclick="openRename(<?= $c['id'] ?>,'<?= h(addslashes($c['name'])) ?>')" class="btn btn-secondary btn-sm" title="Renommer"><i class="fas fa-pen"></i></button>
|
||||
<?php if ($c['nb'] == 0): ?>
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Supprimer cette catégorie ?')">
|
||||
<input type="hidden" name="_action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $c['id'] ?>">
|
||||
<button class="btn btn-danger btn-sm" title="Supprimer"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<button class="btn btn-danger btn-sm" disabled title="Catégorie utilisée" style="opacity:.35"><i class="fas fa-trash"></i></button>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Modal ajouter -->
|
||||
<div id="addModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:999;align-items:center;justify-content:center">
|
||||
<div class="card" style="width:360px">
|
||||
<div style="font-weight:700;margin-bottom:1rem">Nouvelle catégorie</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="_action" value="add">
|
||||
<div class="form-group"><label>Nom</label><input type="text" name="name" autofocus required></div>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1"><i class="fas fa-plus"></i> Ajouter</button>
|
||||
<button type="button" onclick="document.getElementById('addModal').style.display='none'" class="btn btn-secondary">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal renommer -->
|
||||
<div id="renameModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:999;align-items:center;justify-content:center">
|
||||
<div class="card" style="width:360px">
|
||||
<div style="font-weight:700;margin-bottom:1rem">Renommer la catégorie</div>
|
||||
<form method="post" id="renameForm">
|
||||
<input type="hidden" name="_action" value="rename">
|
||||
<input type="hidden" name="id" id="renameId">
|
||||
<div class="form-group"><label>Nouveau nom</label><input type="text" name="name" id="renameName" required></div>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1"><i class="fas fa-check"></i> Enregistrer</button>
|
||||
<button type="button" onclick="document.getElementById('renameModal').style.display='none'" class="btn btn-secondary">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openRename(id, name) {
|
||||
document.getElementById('renameId').value = id;
|
||||
document.getElementById('renameName').value = name;
|
||||
document.getElementById('renameModal').style.display = 'flex';
|
||||
setTimeout(()=>document.getElementById('renameName').focus(),50);
|
||||
}
|
||||
</script>
|
||||
<?php layout_foot(); ?>
|
||||
@@ -0,0 +1,24 @@
|
||||
services:
|
||||
percovitrine:
|
||||
build: .
|
||||
container_name: percovitrine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DB_PATH: /data/vitrine.db
|
||||
UPLOAD_DIR: /data/uploads/
|
||||
volumes:
|
||||
- percovitrine_data:/data
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.percovitrine.rule=Host(`vitrine.perco.home`)"
|
||||
- "traefik.http.routers.percovitrine.entrypoints=web"
|
||||
- "traefik.http.services.percovitrine.loadbalancer.server.port=80"
|
||||
networks:
|
||||
- traefik
|
||||
|
||||
volumes:
|
||||
percovitrine_data:
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
@@ -0,0 +1,16 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html
|
||||
DirectoryIndex index.php
|
||||
|
||||
<Directory /var/www/html>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /var/www/html/data>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
|
||||
$f = basename($_GET['f'] ?? '');
|
||||
if (!$f) { http_response_code(400); exit; }
|
||||
|
||||
$path = $UPLOAD_DIR . $f;
|
||||
if (!is_file($path)) { http_response_code(404); exit; }
|
||||
|
||||
$ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
|
||||
$mime = match($ext) {
|
||||
'jpg','jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'avif' => 'image/avif',
|
||||
'mp4' => 'video/mp4',
|
||||
'webm' => 'video/webm',
|
||||
'mov' => 'video/quicktime',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
|
||||
header('Content-Type: ' . $mime);
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require __DIR__ . '/includes/helpers.php';
|
||||
require __DIR__ . '/includes/layout.php';
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$product = null;
|
||||
$medias = [];
|
||||
|
||||
if ($id) {
|
||||
$product = $pdo->prepare("SELECT * FROM products WHERE id=?")->execute([$id])
|
||||
? $pdo->query("SELECT * FROM products WHERE id=$id")->fetch()
|
||||
: null;
|
||||
if (!$product) { header('Location: /'); exit; }
|
||||
$medias = $pdo->query("SELECT * FROM product_media WHERE product_id=$id ORDER BY sort_order")->fetchAll();
|
||||
}
|
||||
|
||||
/* ─── Sauvegarde ─────────────────────────────────────────── */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$desc = trim($_POST['desc'] ?? '');
|
||||
$price = (float)str_replace(',','.',($_POST['price'] ?? 0));
|
||||
$shipping = (float)str_replace(',','.',($_POST['shipping'] ?? 0));
|
||||
$sku = trim($_POST['sku'] ?? '');
|
||||
$cat_id = (int)($_POST['category_id'] ?? 0) ?: null;
|
||||
$status = $_POST['status'] ?? 'brouillon';
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
|
||||
if (!$title) { $error = 'Le titre est obligatoire.'; goto show; }
|
||||
|
||||
if ($id) {
|
||||
$pdo->prepare("UPDATE products SET title=?,description=?,price=?,shipping=?,sku=?,category_id=?,status=?,tags=?,updated_at=CURRENT_TIMESTAMP WHERE id=?")
|
||||
->execute([$title,$desc,$price,$shipping,$sku,$cat_id,$status,$tags,$id]);
|
||||
} else {
|
||||
$pdo->prepare("INSERT INTO products (title,description,price,shipping,sku,category_id,status,tags) VALUES(?,?,?,?,?,?,?,?)")
|
||||
->execute([$title,$desc,$price,$shipping,$sku,$cat_id,$status,$tags]);
|
||||
$id = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Uploads médias
|
||||
if (!empty($_FILES['medias']['name'][0])) {
|
||||
$count = count($_FILES['medias']['name']);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if ($_FILES['medias']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
||||
$orig = $_FILES['medias']['name'][$i];
|
||||
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, array_merge(imgExts(), vidExts()))) continue;
|
||||
$fname = uniqid('m', true) . '.' . $ext;
|
||||
if (!move_uploaded_file($_FILES['medias']['tmp_name'][$i], $UPLOAD_DIR . $fname)) continue;
|
||||
$type = isImage($fname) ? 'image' : 'video';
|
||||
$order = (int)$pdo->query("SELECT COALESCE(MAX(sort_order),0)+1 FROM product_media WHERE product_id=$id")->fetchColumn();
|
||||
$pdo->prepare("INSERT INTO product_media (product_id,type,filename,original_name,sort_order) VALUES(?,?,?,?,?)")
|
||||
->execute([$id,$type,$fname,$orig,$order]);
|
||||
}
|
||||
}
|
||||
|
||||
// Suppression médias cochés
|
||||
if (!empty($_POST['del_media'])) {
|
||||
foreach ($_POST['del_media'] as $mid) {
|
||||
$mid = (int)$mid;
|
||||
$m = $pdo->query("SELECT filename FROM product_media WHERE id=$mid")->fetch();
|
||||
if ($m && $m['filename']) @unlink($UPLOAD_DIR . $m['filename']);
|
||||
$pdo->prepare("DELETE FROM product_media WHERE id=?")->execute([$mid]);
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: /produit.php?id=' . $id);
|
||||
exit;
|
||||
}
|
||||
|
||||
show:
|
||||
$cats = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
|
||||
$pageTitle = $id ? 'Modifier le produit' : 'Nouveau produit';
|
||||
layout_head($pageTitle);
|
||||
layout_sidebar($id ? '' : 'products');
|
||||
layout_main_open();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title"><?= $id ? 'Modifier' : 'Nouveau produit' ?></div>
|
||||
<?php if ($id): ?><div class="page-sub"><a href="/produit.php?id=<?= $id ?>" style="color:var(--muted);text-decoration:none">← Retour au produit</a></div><?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($error)): ?><div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;color:var(--red)"><?= h($error) ?></div><?php endif ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 360px;gap:1.5rem;align-items:start">
|
||||
|
||||
<!-- Colonne gauche -->
|
||||
<div style="display:flex;flex-direction:column;gap:1rem">
|
||||
|
||||
<div class="card">
|
||||
<div class="form-group">
|
||||
<label>Titre *</label>
|
||||
<input type="text" name="title" value="<?= h($product['title'] ?? '') ?>" placeholder="Nom du produit" required>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Description</label>
|
||||
<textarea name="desc" rows="8" placeholder="Décrivez votre produit (sera envoyé sur Etsy)…"><?= h($product['description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="font-size:.85rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem"><i class="fas fa-images" style="margin-right:.4rem"></i>Photos & vidéos</div>
|
||||
|
||||
<?php if ($medias): ?>
|
||||
<div class="media-grid" id="mediaGrid" style="margin-bottom:1rem">
|
||||
<?php foreach ($medias as $m): ?>
|
||||
<div class="media-item<?= $m['type']==='video'?' video-item':'' ?>" data-id="<?= $m['id'] ?>">
|
||||
<?php if ($m['filename']): ?>
|
||||
<?php if ($m['type']==='image'): ?>
|
||||
<img src="/file.php?f=<?= urlencode($m['filename']) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<video src="/file.php?f=<?= urlencode($m['filename']) ?>" muted playsinline></video>
|
||||
<?php endif ?>
|
||||
<?php elseif ($m['video_url']): ?>
|
||||
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:.5rem;font-size:.7rem;word-break:break-all;color:var(--muted)"><?= h($m['video_url']) ?></div>
|
||||
<?php endif ?>
|
||||
<span class="sort-handle" title="Drag pour réordonner"><i class="fas fa-grip-vertical"></i></span>
|
||||
<label style="all:unset"><input type="checkbox" name="del_media[]" value="<?= $m['id'] ?>" style="position:absolute;top:4px;right:4px;cursor:pointer;width:16px;height:16px" title="Supprimer ce média"></label>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<p class="form-hint" style="margin-bottom:.75rem"><i class="fas fa-square-check" style="margin-right:.3rem"></i>Cocher un média pour le supprimer à la sauvegarde</p>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<input type="file" name="medias[]" multiple accept="image/*,video/mp4,video/webm,video/quicktime" id="fileInput">
|
||||
<i class="fas fa-cloud-arrow-up" style="font-size:1.5rem;margin-bottom:.5rem;display:block"></i>
|
||||
<strong>Cliquer ou glisser</strong> des images / vidéos<br>
|
||||
<span style="font-size:.75rem">JPG, PNG, WEBP, AVIF, GIF, MP4, WEBM, MOV</span>
|
||||
</div>
|
||||
<div class="preview-strip" id="previewStrip"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite -->
|
||||
<div style="display:flex;flex-direction:column;gap:1rem">
|
||||
|
||||
<div class="card">
|
||||
<div style="font-size:.85rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem"><i class="fas fa-euro-sign" style="margin-right:.4rem"></i>Prix</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Prix de vente</label>
|
||||
<div class="input-group">
|
||||
<span class="input-prefix">€</span>
|
||||
<input type="number" name="price" value="<?= h((string)($product['price'] ?? '0')) ?>" step="0.01" min="0" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Frais de port</label>
|
||||
<div class="input-group">
|
||||
<span class="input-prefix">€</span>
|
||||
<input type="number" name="shipping" value="<?= h((string)($product['shipping'] ?? '0')) ?>" step="0.01" min="0" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="font-size:.85rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem"><i class="fas fa-layer-group" style="margin-right:.4rem"></i>Organisation</div>
|
||||
<div class="form-group">
|
||||
<label>Statut</label>
|
||||
<select name="status">
|
||||
<?php foreach (['brouillon'=>'Brouillon','prêt'=>'Prêt à publier','publié'=>'Publié'] as $v=>$l): ?>
|
||||
<option value="<?= $v ?>"<?= ($product['status']??'brouillon')===$v?' selected':'' ?>><?= $l ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Catégorie</label>
|
||||
<select name="category_id">
|
||||
<option value="">— Aucune —</option>
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"<?= ($product['category_id']??'')==$c['id']?' selected':'' ?>><?= h($c['name']) ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
<div class="form-hint">Gérer les catégories dans <a href="/categories.php" style="color:var(--primary)">Catégories</a></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Référence / SKU</label>
|
||||
<input type="text" name="sku" value="<?= h($product['sku'] ?? '') ?>" placeholder="REF-001">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Tags <span style="font-weight:400;text-transform:none">(séparés par des virgules)</span></label>
|
||||
<input type="text" name="tags" value="<?= h($product['tags'] ?? '') ?>" placeholder="artisanat, fait main, cadeau…">
|
||||
<div class="form-hint">Les tags seront utilisés lors de la publication Etsy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($id && ($product['etsy_listing_id'] ?? '')): ?>
|
||||
<div class="etsy-panel">
|
||||
<div class="etsy-logo"><i class="fab fa-etsy"></i> Etsy</div>
|
||||
<div class="mt-2 etsy-status-ok"><i class="fas fa-check-circle"></i> Publié — ID <?= h($product['etsy_listing_id']) ?></div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div style="display:flex;gap:.75rem">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1"><i class="fas fa-save"></i> Sauvegarder</button>
|
||||
<a href="<?= $id ? '/produit.php?id='.$id : '/' ?>" class="btn btn-secondary"><i class="fas fa-xmark"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div><!-- /grid -->
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Preview médias avant upload
|
||||
const input = document.getElementById('fileInput');
|
||||
const strip = document.getElementById('previewStrip');
|
||||
const drop = document.getElementById('dropZone');
|
||||
let files = [];
|
||||
|
||||
function addFiles(newFiles) {
|
||||
[...newFiles].forEach(f => {
|
||||
files.push(f);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'preview-item';
|
||||
const rm = document.createElement('button');
|
||||
rm.type = 'button'; rm.className = 'rm'; rm.innerHTML = '×';
|
||||
rm.onclick = () => { files = files.filter(x=>x!==f); div.remove(); syncInput(); };
|
||||
const tag = f.type.startsWith('image') ? document.createElement('img') : document.createElement('video');
|
||||
tag.src = URL.createObjectURL(f);
|
||||
if (!f.type.startsWith('image')) { tag.muted = true; tag.loop = true; tag.autoplay = true; }
|
||||
div.appendChild(tag); div.appendChild(rm); strip.appendChild(div);
|
||||
});
|
||||
syncInput();
|
||||
}
|
||||
function syncInput() {
|
||||
const dt = new DataTransfer();
|
||||
files.forEach(f => dt.items.add(f));
|
||||
input.files = dt.files;
|
||||
}
|
||||
input.addEventListener('change', () => { addFiles(input.files); });
|
||||
drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('drag-over'); });
|
||||
drop.addEventListener('dragleave', () => drop.classList.remove('drag-over'));
|
||||
drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('drag-over'); addFiles(e.dataTransfer.files); });
|
||||
</script>
|
||||
<?php layout_foot(); ?>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
$DB_PATH = getenv('DB_PATH') ?: '/data/vitrine.db';
|
||||
$UPLOAD_DIR = getenv('UPLOAD_DIR') ?: '/data/uploads/';
|
||||
|
||||
if (!is_dir($UPLOAD_DIR)) @mkdir($UPLOAD_DIR, 0755, true);
|
||||
if (!is_dir(dirname($DB_PATH))) @mkdir(dirname($DB_PATH), 0755, true);
|
||||
|
||||
$pdo = new PDO('sqlite:' . $DB_PATH, null, null, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$pdo->exec("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;");
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
price REAL DEFAULT 0,
|
||||
shipping REAL DEFAULT 0,
|
||||
sku TEXT DEFAULT '',
|
||||
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||
status TEXT DEFAULT 'brouillon' CHECK(status IN ('brouillon','prêt','publié')),
|
||||
etsy_listing_id TEXT DEFAULT '',
|
||||
tags TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_media (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
type TEXT DEFAULT 'image' CHECK(type IN ('image','video')),
|
||||
filename TEXT,
|
||||
original_name TEXT,
|
||||
video_url TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT DEFAULT ''
|
||||
);
|
||||
");
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
function ok(mixed $data): never {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => true, 'data' => $data], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function err(string $msg, int $code = 400): never {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => false, 'error' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
function fmtPrice(float $p): string {
|
||||
return number_format($p, 2, ',', ' ') . ' €';
|
||||
}
|
||||
|
||||
function imgExts(): array { return ['jpg','jpeg','png','gif','webp','avif']; }
|
||||
function vidExts(): array { return ['mp4','webm','mov']; }
|
||||
|
||||
function isImage(string $f): bool {
|
||||
return in_array(strtolower(pathinfo($f, PATHINFO_EXTENSION)), imgExts());
|
||||
}
|
||||
function isVideo(string $f): bool {
|
||||
return in_array(strtolower(pathinfo($f, PATHINFO_EXTENSION)), vidExts());
|
||||
}
|
||||
|
||||
function handleUpload(string $field, string $dir): ?array {
|
||||
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK) return null;
|
||||
$orig = $_FILES[$field]['name'];
|
||||
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
||||
$ok = array_merge(imgExts(), vidExts());
|
||||
if (!in_array($ext, $ok)) return null;
|
||||
$fname = uniqid('m', true) . '.' . $ext;
|
||||
if (!move_uploaded_file($_FILES[$field]['tmp_name'], $dir . $fname)) return null;
|
||||
return ['filename' => $fname, 'original_name' => $orig, 'size' => filesize($dir . $fname)];
|
||||
}
|
||||
|
||||
function statusBadge(string $s): string {
|
||||
$map = [
|
||||
'brouillon' => ['label' => 'Brouillon', 'class' => 'badge-muted'],
|
||||
'prêt' => ['label' => 'Prêt', 'class' => 'badge-volt'],
|
||||
'publié' => ['label' => 'Publié', 'class' => 'badge-green'],
|
||||
];
|
||||
$b = $map[$s] ?? ['label' => $s, 'class' => 'badge-muted'];
|
||||
return '<span class="badge ' . $b['class'] . '">' . h($b['label']) . '</span>';
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
function layout_head(string $title): void {
|
||||
echo '<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>' . htmlspecialchars($title) . ' — PercoVitrine</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head><body><div class="app-wrap">';
|
||||
}
|
||||
|
||||
function layout_sidebar(string $active = ''): void {
|
||||
$links = [
|
||||
['href'=>'/','icon'=>'fa-store','label'=>'Produits','key'=>'products'],
|
||||
['href'=>'/categories.php','icon'=>'fa-tags','label'=>'Catégories','key'=>'categories'],
|
||||
['href'=>'/settings.php','icon'=>'fa-gear','label'=>'Réglages / Etsy','key'=>'settings'],
|
||||
];
|
||||
echo '<aside class="sidebar"><a href="/" class="sidebar-brand"><i class="fas fa-store"></i> <span>PercoVitrine</span></a><nav class="sidebar-nav">';
|
||||
echo '<div class="sidebar-section">Menu</div>';
|
||||
foreach ($links as $l) {
|
||||
$cls = $active === $l['key'] ? ' active' : '';
|
||||
echo '<a href="' . $l['href'] . '" class="sidebar-link' . $cls . '"><i class="fas ' . $l['icon'] . '"></i><span>' . $l['label'] . '</span></a>';
|
||||
}
|
||||
echo '</nav></aside>';
|
||||
}
|
||||
|
||||
function layout_main_open(): void {
|
||||
echo '<main class="main">';
|
||||
}
|
||||
|
||||
function layout_foot(): void {
|
||||
echo '</main></div><div id="toasts" class="toast-wrap"></div>
|
||||
<script>
|
||||
function toast(msg,type="ok"){const c=document.getElementById("toasts");const t=document.createElement("div");t.className="toast"+(type==="error"?" error":"");t.textContent=msg;c.appendChild(t);setTimeout(()=>t.remove(),3500);}
|
||||
function confirm2(msg){return new Promise(r=>{if(confirm(msg))r(true);else r(false);})}
|
||||
</script></body></html>';
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require __DIR__ . '/includes/helpers.php';
|
||||
require __DIR__ . '/includes/layout.php';
|
||||
|
||||
$status = $_GET['status'] ?? '';
|
||||
$cat = $_GET['cat'] ?? '';
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
|
||||
$where = []; $params = [];
|
||||
if ($status) { $where[] = 'p.status = ?'; $params[] = $status; }
|
||||
if ($cat) { $where[] = 'p.category_id = ?'; $params[] = (int)$cat; }
|
||||
if ($q) { $where[] = '(p.title LIKE ? OR p.sku LIKE ? OR p.tags LIKE ?)'; $params = array_merge($params, ["%$q%","%$q%","%$q%"]); }
|
||||
|
||||
$sql = "SELECT p.*, c.name as cat_name,
|
||||
(SELECT filename FROM product_media WHERE product_id=p.id AND type='image' ORDER BY sort_order LIMIT 1) as thumb
|
||||
FROM products p LEFT JOIN categories c ON c.id=p.category_id"
|
||||
. ($where ? ' WHERE '.implode(' AND ',$where) : '')
|
||||
. ' ORDER BY p.updated_at DESC';
|
||||
$stmt = $pdo->prepare($sql); $stmt->execute($params);
|
||||
$products = $stmt->fetchAll();
|
||||
|
||||
$cats = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
|
||||
$total = count($products);
|
||||
|
||||
layout_head('Produits');
|
||||
layout_sidebar('products');
|
||||
layout_main_open();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title"><i class="fas fa-store" style="color:var(--primary);margin-right:.4rem"></i>Mes produits</div>
|
||||
<div class="page-sub"><?= $total ?> produit<?= $total!==1?'s':'' ?></div>
|
||||
</div>
|
||||
<a href="/form.php" class="btn btn-primary"><i class="fas fa-plus"></i> Nouveau produit</a>
|
||||
</div>
|
||||
|
||||
<!-- Filtres -->
|
||||
<div class="filters">
|
||||
<form method="get" style="display:contents">
|
||||
<input type="text" name="q" value="<?= h($q) ?>" placeholder="Rechercher…" style="width:200px;height:34px;padding:.3rem .7rem">
|
||||
<?php if ($status||$cat) echo '<input type="hidden" name="status" value="'.h($status).'"><input type="hidden" name="cat" value="'.h($cat).'">'; ?>
|
||||
</form>
|
||||
<a href="/" class="filter-chip<?= !$status&&!$cat&&!$q?' active':'' ?>">Tous</a>
|
||||
<a href="?status=brouillon<?= $q?"&q=".urlencode($q):'' ?>" class="filter-chip<?= $status==='brouillon'?' active':'' ?>">Brouillons</a>
|
||||
<a href="?status=prêt<?= $q?"&q=".urlencode($q):'' ?>" class="filter-chip<?= $status==='prêt'?' active':'' ?>">Prêts</a>
|
||||
<a href="?status=publié<?= $q?"&q=".urlencode($q):'' ?>" class="filter-chip<?= $status==='publié'?' active':'' ?>">Publiés</a>
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<a href="?cat=<?= $c['id'] ?><?= $q?"&q=".urlencode($q):'' ?>" class="filter-chip<?= $cat==(string)$c['id']?' active':'' ?>"><?= h($c['name']) ?></a>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<?php if (!$products): ?>
|
||||
<div class="empty-state"><i class="fas fa-store"></i><p>Aucun produit pour l'instant.</p><a href="/form.php" class="btn btn-primary mt-3"><i class="fas fa-plus"></i> Créer mon premier produit</a></div>
|
||||
<?php else: ?>
|
||||
<div class="products-grid">
|
||||
<?php foreach ($products as $p): ?>
|
||||
<div class="product-card" style="cursor:default">
|
||||
<?php if ($p['thumb']): ?>
|
||||
<img src="/file.php?f=<?= urlencode($p['thumb']) ?>" alt="" class="product-thumb">
|
||||
<?php else: ?>
|
||||
<div class="product-thumb-placeholder"><i class="fas fa-image"></i></div>
|
||||
<?php endif ?>
|
||||
<div class="product-body">
|
||||
<div class="product-title"><?= h($p['title']) ?></div>
|
||||
<?php if ($p['cat_name']): ?><div class="product-cat"><i class="fas fa-tag" style="font-size:.65rem"></i> <?= h($p['cat_name']) ?></div><?php endif ?>
|
||||
<?php if ($p['sku']): ?><div class="product-sku">Réf : <?= h($p['sku']) ?></div><?php endif ?>
|
||||
<div class="product-meta">
|
||||
<span class="product-price"><?= fmtPrice($p['price']) ?></span>
|
||||
<?= statusBadge($p['status']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-actions">
|
||||
<a href="/produit.php?id=<?= $p['id'] ?>" class="btn btn-secondary btn-sm" title="Voir"><i class="fas fa-eye"></i></a>
|
||||
<a href="/form.php?id=<?= $p['id'] ?>" class="btn btn-secondary btn-sm" title="Modifier"><i class="fas fa-pen"></i></a>
|
||||
<button onclick="deleteProduct(<?= $p['id'] ?>,this)" class="btn btn-danger btn-sm" title="Supprimer"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<script>
|
||||
async function deleteProduct(id, btn) {
|
||||
if (!confirm('Supprimer ce produit définitivement ?')) return;
|
||||
const r = await fetch('/api.php?action=product&id=' + id, {method:'DELETE'});
|
||||
const j = await r.json();
|
||||
if (j.ok) { btn.closest('.product-card').remove(); toast('Produit supprimé'); }
|
||||
else toast(j.error || 'Erreur', 'error');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php layout_foot(); ?>
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require __DIR__ . '/includes/helpers.php';
|
||||
require __DIR__ . '/includes/layout.php';
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) { header('Location: /'); exit; }
|
||||
|
||||
$p = $pdo->query("SELECT p.*, c.name as cat_name FROM products p LEFT JOIN categories c ON c.id=p.category_id WHERE p.id=$id")->fetch();
|
||||
if (!$p) { header('Location: /'); exit; }
|
||||
|
||||
$medias = $pdo->query("SELECT * FROM product_media WHERE product_id=$id ORDER BY sort_order")->fetchAll();
|
||||
$images = array_filter($medias, fn($m) => $m['type']==='image' && $m['filename']);
|
||||
$videos = array_filter($medias, fn($m) => $m['type']==='video');
|
||||
|
||||
$etsyKey = $pdo->query("SELECT value FROM settings WHERE key='etsy_api_key'")->fetchColumn();
|
||||
|
||||
layout_head(h($p['title']));
|
||||
layout_sidebar('');
|
||||
layout_main_open();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<a href="/" style="color:var(--muted);text-decoration:none;font-size:.85rem">← Tous les produits</a>
|
||||
<div class="page-title" style="margin-top:.25rem"><?= h($p['title']) ?></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<a href="/form.php?id=<?= $id ?>" class="btn btn-secondary"><i class="fas fa-pen"></i> Modifier</a>
|
||||
<?php if ($etsyKey && $p['status']==='prêt' && !$p['etsy_listing_id']): ?>
|
||||
<button onclick="publishEtsy(<?= $id ?>)" class="btn btn-success"><i class="fab fa-etsy"></i> Publier sur Etsy</button>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-detail-grid">
|
||||
|
||||
<!-- Médias -->
|
||||
<div>
|
||||
<?php
|
||||
$allMedia = array_merge(array_values($images), array_values($videos));
|
||||
$first = $allMedia[0] ?? null;
|
||||
?>
|
||||
<div class="media-main" id="mediaMain">
|
||||
<?php if ($first): ?>
|
||||
<?php if ($first['type']==='image'): ?>
|
||||
<img src="/file.php?f=<?= urlencode($first['filename']) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<video src="<?= $first['filename'] ? '/file.php?f='.urlencode($first['filename']) : h($first['video_url']) ?>" controls style="width:100%;border-radius:10px;aspect-ratio:1;object-fit:cover"></video>
|
||||
<?php endif ?>
|
||||
<?php else: ?>
|
||||
<div style="aspect-ratio:1;background:var(--bg-card);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--border);font-size:4rem"><i class="fas fa-image"></i></div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php if (count($allMedia) > 1): ?>
|
||||
<div class="media-thumbs">
|
||||
<?php foreach ($allMedia as $i => $m): ?>
|
||||
<div class="media-thumb<?= $i===0?' active':'' ?>" onclick="switchMedia(this,'<?= $m['type'] ?>','<?= $m['filename'] ? '/file.php?f='.urlencode($m['filename']) : h($m['video_url']) ?>')">
|
||||
<?php if ($m['type']==='image'): ?>
|
||||
<img src="/file.php?f=<?= urlencode($m['filename']) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<div style="width:100%;height:100%;background:var(--bg);display:flex;align-items:center;justify-content:center;color:var(--muted)"><i class="fas fa-play"></i></div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($p['description']): ?>
|
||||
<div class="card" style="margin-top:1.25rem">
|
||||
<div style="font-size:.75rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:.75rem">Description</div>
|
||||
<div class="desc-block"><?= nl2br(h($p['description'])) ?></div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<!-- Infos produit -->
|
||||
<div class="product-info-card">
|
||||
<div class="card">
|
||||
<div class="price-block"><?= fmtPrice($p['price']) ?></div>
|
||||
<?php if ($p['shipping'] > 0): ?>
|
||||
<div class="shipping-block" style="margin-top:.35rem"><i class="fas fa-truck" style="margin-right:.3rem"></i>Frais de port : <?= fmtPrice($p['shipping']) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="shipping-block" style="margin-top:.35rem"><i class="fas fa-truck" style="margin-right:.3rem"></i>Livraison gratuite</div>
|
||||
<?php endif ?>
|
||||
<div style="margin-top:.75rem"><?= statusBadge($p['status']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<?php if ($p['cat_name']): ?>
|
||||
<div class="info-row"><span class="info-label">Catégorie</span><span><?= h($p['cat_name']) ?></span></div>
|
||||
<?php endif ?>
|
||||
<?php if ($p['sku']): ?>
|
||||
<div class="info-row"><span class="info-label">Référence</span><code style="font-size:.8rem"><?= h($p['sku']) ?></code></div>
|
||||
<?php endif ?>
|
||||
<?php if ($p['tags']): ?>
|
||||
<div class="info-row"><span class="info-label">Tags</span><span style="text-align:right;color:var(--muted);font-size:.8rem"><?= h($p['tags']) ?></span></div>
|
||||
<?php endif ?>
|
||||
<div class="info-row"><span class="info-label">Créé le</span><span style="font-size:.82rem;color:var(--muted)"><?= date('d/m/Y', strtotime($p['created_at'])) ?></span></div>
|
||||
<div class="info-row"><span class="info-label">Modifié</span><span style="font-size:.82rem;color:var(--muted)"><?= date('d/m/Y H:i', strtotime($p['updated_at'])) ?></span></div>
|
||||
<?php if ($p['etsy_listing_id']): ?>
|
||||
<div class="info-row"><span class="info-label">Etsy ID</span><span style="font-size:.82rem;font-family:monospace"><?= h($p['etsy_listing_id']) ?></span></div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<?php if ($etsyKey): ?>
|
||||
<div class="etsy-panel">
|
||||
<div class="etsy-logo"><i class="fab fa-etsy"></i> Etsy</div>
|
||||
<?php if ($p['etsy_listing_id']): ?>
|
||||
<div class="mt-2 etsy-status-ok"><i class="fas fa-check-circle"></i> Publié sur Etsy (ID <?= h($p['etsy_listing_id']) ?>)</div>
|
||||
<?php elseif ($p['status']==='prêt'): ?>
|
||||
<div class="mt-2" style="font-size:.82rem;color:var(--muted)">Prêt à publier — clique sur "Publier sur Etsy"</div>
|
||||
<?php else: ?>
|
||||
<div class="mt-2 etsy-status-ko"><i class="fas fa-clock"></i> Statut "prêt" requis pour publier</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="etsy-panel">
|
||||
<div class="etsy-logo"><i class="fab fa-etsy"></i> Etsy</div>
|
||||
<div class="mt-2 etsy-status-ko"><i class="fas fa-key"></i> Clé API non configurée — <a href="/settings.php" style="color:var(--primary)">Réglages</a></div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<a href="/form.php?id=<?= $id ?>" class="btn btn-secondary" style="flex:1"><i class="fas fa-pen"></i> Modifier</a>
|
||||
<button onclick="if(confirm('Supprimer ce produit ?'))deleteProduct(<?= $id ?>)" class="btn btn-danger"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchMedia(thumb, type, src) {
|
||||
document.querySelectorAll('.media-thumb').forEach(t=>t.classList.remove('active'));
|
||||
thumb.classList.add('active');
|
||||
const main = document.getElementById('mediaMain');
|
||||
if (type === 'image') {
|
||||
main.innerHTML = `<img src="${src}" alt="" style="width:100%;border-radius:10px;aspect-ratio:1;object-fit:cover">`;
|
||||
} else {
|
||||
main.innerHTML = `<video src="${src}" controls style="width:100%;border-radius:10px;aspect-ratio:1;object-fit:cover"></video>`;
|
||||
}
|
||||
}
|
||||
async function deleteProduct(id) {
|
||||
const r = await fetch('/api.php?action=product&id='+id, {method:'DELETE'});
|
||||
const j = await r.json();
|
||||
if (j.ok) window.location='/';
|
||||
else toast(j.error||'Erreur','error');
|
||||
}
|
||||
async function publishEtsy(id) {
|
||||
toast('Publication Etsy pas encore disponible (API en cours d\'intégration)');
|
||||
}
|
||||
</script>
|
||||
<?php layout_foot(); ?>
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require __DIR__ . '/includes/helpers.php';
|
||||
require __DIR__ . '/includes/layout.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$keys = ['etsy_api_key','etsy_api_secret','etsy_shop_id','currency'];
|
||||
$stmt = $pdo->prepare("INSERT OR REPLACE INTO settings(key,value) VALUES(?,?)");
|
||||
foreach ($keys as $k) {
|
||||
$stmt->execute([$k, trim($_POST[$k] ?? '')]);
|
||||
}
|
||||
header('Location: /settings.php?saved=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
function cfg(PDO $pdo, string $key): string {
|
||||
$v = $pdo->prepare("SELECT value FROM settings WHERE key=?");
|
||||
$v->execute([$key]);
|
||||
return $v->fetchColumn() ?: '';
|
||||
}
|
||||
|
||||
layout_head('Réglages');
|
||||
layout_sidebar('settings');
|
||||
layout_main_open();
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-title"><i class="fas fa-gear" style="color:var(--primary);margin-right:.4rem"></i>Réglages</div>
|
||||
</div>
|
||||
|
||||
<?php if ($_GET['saved'] ?? false): ?>
|
||||
<div style="background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;color:var(--green)"><i class="fas fa-check-circle"></i> Réglages enregistrés.</div>
|
||||
<?php endif ?>
|
||||
|
||||
<form method="post">
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:1rem">
|
||||
<!-- Etsy -->
|
||||
<div class="card">
|
||||
<div class="etsy-logo mb-3"><i class="fab fa-etsy"></i> Intégration Etsy</div>
|
||||
<p style="font-size:.83rem;color:var(--muted);margin-bottom:1rem;line-height:1.6">
|
||||
Pour obtenir une clé API Etsy, va sur
|
||||
<strong>etsy.com/developers</strong>, crée une application, et
|
||||
copie la clé et le secret ci-dessous.<br>
|
||||
L'intégration permettra de publier tes fiches directement depuis PercoVitrine.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label>API Key (Keystring)</label>
|
||||
<input type="text" name="etsy_api_key" value="<?= h(cfg($pdo,'etsy_api_key')) ?>" placeholder="Coller la clé API…" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Secret</label>
|
||||
<input type="password" name="etsy_api_secret" value="<?= h(cfg($pdo,'etsy_api_secret')) ?>" placeholder="Coller le secret…" autocomplete="off">
|
||||
<div class="form-hint">Stocké localement, jamais transmis sauf à Etsy</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Shop ID</label>
|
||||
<input type="text" name="etsy_shop_id" value="<?= h(cfg($pdo,'etsy_shop_id')) ?>" placeholder="Nom ou ID de ta boutique Etsy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:1rem">
|
||||
<!-- Général -->
|
||||
<div class="card">
|
||||
<div style="font-size:.85rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem"><i class="fas fa-sliders" style="margin-right:.4rem"></i>Général</div>
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Devise</label>
|
||||
<select name="currency">
|
||||
<?php foreach (['EUR'=>'Euro (€)','USD'=>'Dollar ($)','GBP'=>'Livre sterling (£)','CHF'=>'Franc suisse (CHF)'] as $v=>$l): ?>
|
||||
<option value="<?= $v ?>"<?= cfg($pdo,'currency')===$v?' selected':'' ?>><?= $l ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="background:rgba(45,163,242,.04);border-color:rgba(45,163,242,.15)">
|
||||
<div style="font-size:.85rem;font-weight:700;margin-bottom:.5rem"><i class="fas fa-circle-info" style="color:var(--primary);margin-right:.4rem"></i>Roadmap intégrations</div>
|
||||
<ul style="font-size:.82rem;color:var(--muted);padding-left:1.2rem;line-height:2">
|
||||
<li><span style="color:var(--volt)">●</span> Etsy — en cours d'intégration</li>
|
||||
<li><span style="color:var(--border)">●</span> Vinted — prévu</li>
|
||||
<li><span style="color:var(--border)">●</span> Leboncoin — prévu</li>
|
||||
<li><span style="color:var(--border)">●</span> WooCommerce — prévu</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Enregistrer</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
<?php layout_foot(); ?>
|
||||
Reference in New Issue
Block a user