Deploy from Lumerel
This commit is contained in:
54
.docker/99-migrate.sh
Normal file
54
.docker/99-migrate.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
# IMPORTANT: This script is sourced (not executed) by the webdevops entrypoint.
|
||||
# Never use "exit" — it would kill the parent entrypoint and prevent supervisord from starting.
|
||||
# Use "return" instead to leave this script without affecting the parent.
|
||||
|
||||
echo ">>> Waiting for database connection..."
|
||||
echo ">>> DB_HOST=${DB_HOST:-NOT SET}"
|
||||
echo ">>> DB_PORT=${DB_PORT:-NOT SET}"
|
||||
echo ">>> DB_DATABASE=${DB_DATABASE:-NOT SET}"
|
||||
echo ">>> DB_USERNAME=${DB_USERNAME:-NOT SET}"
|
||||
echo ">>> DB_PASSWORD is $([ -n "$DB_PASSWORD" ] && echo 'set' || echo 'NOT SET')"
|
||||
|
||||
if [ -z "$DB_HOST" ] || [ -z "$DB_USERNAME" ]; then
|
||||
echo ">>> ERROR: DB_HOST or DB_USERNAME not set. Skipping migrations."
|
||||
return 0 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_migrate_max_retries=30
|
||||
_migrate_count=0
|
||||
_migrate_done=false
|
||||
|
||||
while [ $_migrate_count -lt $_migrate_max_retries ]; do
|
||||
if php -r '
|
||||
try {
|
||||
$host = getenv("DB_HOST");
|
||||
$port = getenv("DB_PORT") ?: "3306";
|
||||
$user = getenv("DB_USERNAME");
|
||||
$pass = getenv("DB_PASSWORD");
|
||||
new PDO("mysql:host=$host;port=$port", $user, $pass, [PDO::ATTR_TIMEOUT => 3]);
|
||||
exit(0);
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "PDO error: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
' 2>&1; then
|
||||
echo ">>> Database is reachable. Running migrations..."
|
||||
if php /app/migrate.php; then
|
||||
echo ">>> Migrations completed successfully."
|
||||
else
|
||||
echo ">>> WARNING: migrate.php exited with code $?"
|
||||
fi
|
||||
_migrate_done=true
|
||||
break
|
||||
fi
|
||||
|
||||
_migrate_count=$((_migrate_count + 1))
|
||||
echo ">>> Waiting for database... attempt $_migrate_count/$_migrate_max_retries"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$_migrate_done" = false ]; then
|
||||
echo ">>> WARNING: Could not connect to database after $_migrate_max_retries attempts. Skipping migrations."
|
||||
fi
|
||||
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.git
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM webdevops/php-nginx:8.3-alpine
|
||||
ENV WEB_DOCUMENT_ROOT=/app
|
||||
ARG CACHE_BUST=1774219000
|
||||
COPY . /app
|
||||
RUN echo "index index.php index.html index.htm;" > /opt/docker/etc/nginx/vhost.common.d/01-index.conf \
|
||||
&& echo "add_header Cache-Control 'no-cache, no-store, must-revalidate';" > /opt/docker/etc/nginx/vhost.common.d/02-no-cache.conf
|
||||
RUN set -e; if [ -f /app/composer.json ]; then \
|
||||
echo ">>> composer.json found, installing dependencies..."; \
|
||||
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
|
||||
cd /app && composer install --no-dev --no-interaction --optimize-autoloader; \
|
||||
ls -la /app/vendor/autoload.php; \
|
||||
else \
|
||||
echo ">>> No composer.json found, skipping composer install"; \
|
||||
fi
|
||||
RUN set -e; if [ -f /app/package.json ]; then \
|
||||
echo ">>> package.json found, installing node dependencies..."; \
|
||||
apk add --no-cache nodejs npm; \
|
||||
cd /app && npm install --production; \
|
||||
else \
|
||||
echo ">>> No package.json found, skipping npm install"; \
|
||||
fi
|
||||
COPY --chmod=755 .docker/99-migrate.sh /opt/docker/provision/entrypoint.d/99-migrate.sh
|
||||
RUN chown -R application:application /app
|
||||
189
api.php
Normal file
189
api.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$action = $_GET['action'] ?? $input['action'] ?? null;
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
|
||||
case 'list':
|
||||
$filter = $_GET['filter'] ?? 'all';
|
||||
$service = $_GET['service'] ?? '';
|
||||
$type = $_GET['type'] ?? '';
|
||||
$search = $_GET['search'] ?? '';
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($filter === 'watched') {
|
||||
$where[] = 'watched = 1';
|
||||
} elseif ($filter === 'unwatched') {
|
||||
$where[] = 'watched = 0';
|
||||
}
|
||||
|
||||
if ($service !== '') {
|
||||
$where[] = 'streaming_service = ?';
|
||||
$params[] = $service;
|
||||
}
|
||||
|
||||
if ($type !== '') {
|
||||
$where[] = 'type = ?';
|
||||
$params[] = $type;
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
$where[] = 'title LIKE ?';
|
||||
$params[] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$sql = 'SELECT * FROM watchlist';
|
||||
if ($where) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
$sql .= ' ORDER BY watched ASC, created_at DESC';
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$items = $stmt->fetchAll();
|
||||
|
||||
$stats = $pdo->query("
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(watched) AS watched,
|
||||
COUNT(*) - SUM(watched) AS unwatched
|
||||
FROM watchlist
|
||||
")->fetch();
|
||||
|
||||
$services = $pdo->query("
|
||||
SELECT streaming_service, COUNT(*) AS cnt
|
||||
FROM watchlist
|
||||
WHERE streaming_service IS NOT NULL AND streaming_service != ''
|
||||
GROUP BY streaming_service
|
||||
ORDER BY cnt DESC
|
||||
")->fetchAll();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'items' => $items,
|
||||
'stats' => $stats,
|
||||
'services' => $services,
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
$title = trim($input['title'] ?? '');
|
||||
$type = $input['type'] ?? 'movie';
|
||||
$service = trim($input['streaming_service'] ?? '');
|
||||
$genre = trim($input['genre'] ?? '');
|
||||
$notes = trim($input['notes'] ?? '');
|
||||
|
||||
if ($title === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Title is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO watchlist (title, type, streaming_service, genre, notes)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$title, $type, $service ?: null, $genre ?: null, $notes ?: null]);
|
||||
$id = $pdo->lastInsertId();
|
||||
|
||||
$item = $pdo->prepare("SELECT * FROM watchlist WHERE id = ?");
|
||||
$item->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'item' => $item->fetch()]);
|
||||
break;
|
||||
|
||||
case 'toggle':
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'ID required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$current = $pdo->prepare("SELECT watched FROM watchlist WHERE id = ?");
|
||||
$current->execute([$id]);
|
||||
$row = $current->fetch();
|
||||
|
||||
if (!$row) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'error' => 'Not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$newWatched = $row['watched'] ? 0 : 1;
|
||||
$watchedAt = $newWatched ? date('Y-m-d H:i:s') : null;
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE watchlist SET watched = ?, watched_at = ? WHERE id = ?");
|
||||
$stmt->execute([$newWatched, $watchedAt, $id]);
|
||||
|
||||
$item = $pdo->prepare("SELECT * FROM watchlist WHERE id = ?");
|
||||
$item->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'item' => $item->fetch()]);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
$title = trim($input['title'] ?? '');
|
||||
$type = $input['type'] ?? 'movie';
|
||||
$service = trim($input['streaming_service'] ?? '');
|
||||
$genre = trim($input['genre'] ?? '');
|
||||
$notes = trim($input['notes'] ?? '');
|
||||
|
||||
if (!$id || $title === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'ID and title required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE watchlist SET title = ?, type = ?, streaming_service = ?, genre = ?, notes = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$title, $type, $service ?: null, $genre ?: null, $notes ?: null, $id]);
|
||||
|
||||
$item = $pdo->prepare("SELECT * FROM watchlist WHERE id = ?");
|
||||
$item->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'item' => $item->fetch()]);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$id = (int)($input['id'] ?? 0);
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'ID required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM watchlist WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Unknown action']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
14
db.php
Normal file
14
db.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
$host = getenv('DB_HOST');
|
||||
$port = getenv('DB_PORT') ?: '3306';
|
||||
$db = getenv('DB_DATABASE');
|
||||
$user = getenv('DB_USERNAME');
|
||||
$pass = getenv('DB_PASSWORD');
|
||||
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4",
|
||||
$user,
|
||||
$pass
|
||||
);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
178
index.php
Normal file
178
index.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Watchlist ✨</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Ambient background -->
|
||||
<div class="ambient">
|
||||
<div class="ambient-orb orb-1"></div>
|
||||
<div class="ambient-orb orb-2"></div>
|
||||
<div class="ambient-orb orb-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="app">
|
||||
|
||||
<!-- HEADER -->
|
||||
<header class="header">
|
||||
<div class="header-brand">
|
||||
<span class="header-icon">🎬</span>
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">Watchlist</h1>
|
||||
<p class="header-sub">Your personal streaming guide</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary" id="openAddModal">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add Title
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- STATS -->
|
||||
<section class="stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value" id="statTotal">—</span>
|
||||
<span class="stat-label">Total</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-value" id="statUnwatched">—</span>
|
||||
<span class="stat-label">To Watch</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-value" id="statWatched">—</span>
|
||||
<span class="stat-label">Watched</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat stat-progress">
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<span class="stat-label" id="statPercent">0% complete</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CONTROLS -->
|
||||
<section class="controls">
|
||||
<div class="pill-group" id="filterGroup">
|
||||
<button class="pill active" data-filter="all">All</button>
|
||||
<button class="pill" data-filter="unwatched">To Watch</button>
|
||||
<button class="pill" data-filter="watched">Watched</button>
|
||||
</div>
|
||||
<div class="pill-group" id="typeGroup">
|
||||
<button class="type-pill active" data-type="">All</button>
|
||||
<button class="type-pill" data-type="movie">🎬 Movies</button>
|
||||
<button class="type-pill" data-type="show">📺 Shows</button>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Search…" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SERVICE FILTER PILLS -->
|
||||
<div class="service-row" id="serviceRow"></div>
|
||||
|
||||
<!-- GRID -->
|
||||
<main class="grid" id="grid">
|
||||
<div class="loading" id="loadingState">
|
||||
<div class="loader"></div>
|
||||
<p>Loading…</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- EMPTY STATE -->
|
||||
<div class="empty hidden" id="emptyState">
|
||||
<div class="empty-icon">🍿</div>
|
||||
<h2 class="empty-title">Nothing here yet</h2>
|
||||
<p class="empty-sub">Add your first movie or show to get started.</p>
|
||||
<button class="btn-primary" id="openAddModalEmpty">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add Title
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ADD / EDIT MODAL -->
|
||||
<div class="modal-overlay hidden" id="modalOverlay">
|
||||
<div class="modal" id="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="modalTitle">Add Title</h2>
|
||||
<button class="modal-close" id="closeModal">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="addForm" class="modal-form">
|
||||
<input type="hidden" id="editId" />
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="inputTitle">Title <span class="req">*</span></label>
|
||||
<input type="text" class="form-input" id="inputTitle" placeholder="e.g. Dune: Part Two" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="form-label">Type</label>
|
||||
<div class="toggle-group">
|
||||
<button type="button" class="toggle-btn active" data-val="movie">🎬 Movie</button>
|
||||
<button type="button" class="toggle-btn" data-val="show">📺 Show</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="inputService">Streaming On</label>
|
||||
<input type="text" class="form-input" id="inputService" placeholder="e.g. Netflix, HBO Max…" autocomplete="off" list="serviceList" />
|
||||
<datalist id="serviceList"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="inputGenre">Genre</label>
|
||||
<input type="text" class="form-input" id="inputGenre" placeholder="e.g. Sci-Fi, Drama…" autocomplete="off" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="inputNotes">Notes</label>
|
||||
<input type="text" class="form-input" id="inputNotes" placeholder="Optional notes…" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-ghost" id="cancelModal">Cancel</button>
|
||||
<button type="submit" class="btn-primary" id="submitBtn">Add to Watchlist</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DELETE CONFIRM MODAL -->
|
||||
<div class="modal-overlay hidden" id="deleteOverlay">
|
||||
<div class="modal modal-sm" id="deleteModal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Remove Title?</h2>
|
||||
<button class="modal-close" id="closeDeleteModal">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="delete-msg">Are you sure you want to remove <strong id="deleteTitle"></strong> from your watchlist?</p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-ghost" id="cancelDelete">Cancel</button>
|
||||
<button class="btn-danger" id="confirmDelete">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOAST -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
migrate.php
Normal file
25
migrate.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
migration VARCHAR(255) NOT NULL UNIQUE,
|
||||
ran_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
|
||||
$ran = $pdo->query("SELECT migration FROM migrations")->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$files = glob(__DIR__ . '/migrations/*.php');
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file);
|
||||
if (!in_array($name, $ran)) {
|
||||
require $file;
|
||||
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
|
||||
$stmt->execute([$name]);
|
||||
echo "Ran: {$name}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "Migrations complete.\n";
|
||||
13
migrations/001_create_watchlist_table.php
Normal file
13
migrations/001_create_watchlist_table.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS watchlist (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
type ENUM('movie', 'show') NOT NULL DEFAULT 'movie',
|
||||
streaming_service VARCHAR(100) DEFAULT NULL,
|
||||
genre VARCHAR(100) DEFAULT NULL,
|
||||
notes TEXT DEFAULT NULL,
|
||||
watched TINYINT(1) NOT NULL DEFAULT 0,
|
||||
watched_at TIMESTAMP NULL DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)");
|
||||
Reference in New Issue
Block a user