Init PercoVitrine — app de gestion de fiches produits
This commit is contained in:
@@ -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>';
|
||||
}
|
||||
Reference in New Issue
Block a user