Adds login/logout/session handling, a create-critter form backed by adjectives/nouns/verbs/locations lookup tables, Replicate-generated critter images saved to disk, and a Tailwind-styled critter gallery on the homepage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
236 lines
9.2 KiB
PHP
236 lines
9.2 KiB
PHP
<?php
|
|
require __DIR__ . '/session.php';
|
|
require __DIR__ . '/db.php';
|
|
start_app_session();
|
|
|
|
if (!isset($_SESSION['username']) || !isset($_SESSION['user_id'])) {
|
|
header('Location: /');
|
|
exit;
|
|
}
|
|
|
|
$pdo = getDbConnection();
|
|
|
|
$pdo->exec('CREATE TABLE IF NOT EXISTS critters (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
noun_id INTEGER NOT NULL REFERENCES nouns(id),
|
|
adjective_id INTEGER NOT NULL REFERENCES adjectives(id),
|
|
verb_id INTEGER NOT NULL REFERENCES verbs(id),
|
|
location_id INTEGER NOT NULL REFERENCES locations(id),
|
|
author_id INTEGER NOT NULL REFERENCES users(id),
|
|
image TEXT
|
|
)');
|
|
|
|
$critterColumns = $pdo->query('PRAGMA table_info(critters)')->fetchAll(PDO::FETCH_COLUMN, 1);
|
|
if (!in_array('author_id', $critterColumns, true)) {
|
|
$pdo->exec('ALTER TABLE critters ADD COLUMN author_id INTEGER REFERENCES users(id)');
|
|
}
|
|
if (!in_array('image', $critterColumns, true)) {
|
|
$pdo->exec('ALTER TABLE critters ADD COLUMN image TEXT');
|
|
}
|
|
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_noun_id ON critters (noun_id)');
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_adjective_id ON critters (adjective_id)');
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_verb_id ON critters (verb_id)');
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_location_id ON critters (location_id)');
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_author_id ON critters (author_id)');
|
|
|
|
function rowExistsById(PDO $pdo, string $table, $id): bool
|
|
{
|
|
$stmt = $pdo->prepare("SELECT 1 FROM {$table} WHERE id = :id");
|
|
$stmt->execute(['id' => $id]);
|
|
return (bool) $stmt->fetch();
|
|
}
|
|
|
|
function getValueById(PDO $pdo, string $table, $id): string
|
|
{
|
|
$stmt = $pdo->prepare("SELECT value FROM {$table} WHERE id = :id");
|
|
$stmt->execute(['id' => $id]);
|
|
return (string) $stmt->fetchColumn();
|
|
}
|
|
|
|
$adjectives = $pdo->query('SELECT id, value FROM adjectives ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
|
|
$nouns = $pdo->query('SELECT id, value FROM nouns ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
|
|
$verbs = $pdo->query('SELECT id, value FROM verbs ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
|
|
$locations = $pdo->query('SELECT id, value FROM locations ORDER BY value')->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$errors = [];
|
|
$selected = [
|
|
'adjective_id' => '',
|
|
'noun_id' => '',
|
|
'verb_id' => '',
|
|
'location_id' => '',
|
|
];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$fields = [
|
|
'adjective_id' => ['table' => 'adjectives', 'label' => 'adjective'],
|
|
'noun_id' => ['table' => 'nouns', 'label' => 'noun'],
|
|
'verb_id' => ['table' => 'verbs', 'label' => 'verb'],
|
|
'location_id' => ['table' => 'locations', 'label' => 'location'],
|
|
];
|
|
|
|
foreach ($fields as $field => $info) {
|
|
$value = $_POST[$field] ?? '';
|
|
$selected[$field] = $value;
|
|
|
|
if ($value === '' || !ctype_digit((string) $value) || !rowExistsById($pdo, $info['table'], $value)) {
|
|
$errors[$field] = 'Please choose a valid ' . $info['label'] . '.';
|
|
}
|
|
}
|
|
|
|
if (empty($errors)) {
|
|
$adjectiveValue = getValueById($pdo, 'adjectives', $selected['adjective_id']);
|
|
$nounValue = getValueById($pdo, 'nouns', $selected['noun_id']);
|
|
$verbValue = getValueById($pdo, 'verbs', $selected['verb_id']);
|
|
$locationValue = getValueById($pdo, 'locations', $selected['location_id']);
|
|
|
|
$prompt = "A {$adjectiveValue} {$nounValue} {$verbValue} {$locationValue} in a high quality 3d animation art style. The image should be vibrant and colorful, capturing the essence of the action and setting.";
|
|
|
|
$ch = curl_init('https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions');
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt');
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ' . getenv('REPLICATE_API_TOKEN'),
|
|
'Content-Type: application/json',
|
|
'Prefer: wait',
|
|
]);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
|
'input' => [
|
|
'prompt' => $prompt,
|
|
],
|
|
]));
|
|
|
|
$replicateResponse = curl_exec($ch);
|
|
$imageUrl = null;
|
|
|
|
if ($replicateResponse !== false) {
|
|
$prediction = json_decode($replicateResponse, true);
|
|
|
|
$pollAttempt = 0;
|
|
while (
|
|
isset($prediction['status'])
|
|
&& !in_array($prediction['status'], ['succeeded', 'failed', 'canceled'], true)
|
|
&& $pollAttempt < 30
|
|
) {
|
|
sleep(2);
|
|
|
|
$pollCh = curl_init($prediction['urls']['get']);
|
|
curl_setopt($pollCh, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($pollCh, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt');
|
|
curl_setopt($pollCh, CURLOPT_HTTPHEADER, [
|
|
'Authorization: Bearer ' . getenv('REPLICATE_API_TOKEN'),
|
|
]);
|
|
|
|
$pollResponse = curl_exec($pollCh);
|
|
if ($pollResponse !== false) {
|
|
$prediction = json_decode($pollResponse, true);
|
|
}
|
|
|
|
$pollAttempt++;
|
|
}
|
|
|
|
if (($prediction['status'] ?? null) === 'succeeded') {
|
|
$output = $prediction['output'] ?? null;
|
|
$imageUrl = is_array($output) ? ($output[0] ?? null) : $output;
|
|
}
|
|
}
|
|
|
|
$imageFilename = null;
|
|
|
|
if ($imageUrl !== null) {
|
|
$imageCh = curl_init($imageUrl);
|
|
curl_setopt($imageCh, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($imageCh, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt');
|
|
$imageData = curl_exec($imageCh);
|
|
|
|
if ($imageData !== false) {
|
|
$imagesDir = __DIR__ . '/critter-images';
|
|
if (!is_dir($imagesDir)) {
|
|
mkdir($imagesDir, 0755, true);
|
|
}
|
|
|
|
$imageFilename = bin2hex(random_bytes(16)) . '.webp';
|
|
file_put_contents($imagesDir . '/' . $imageFilename, $imageData);
|
|
}
|
|
}
|
|
|
|
$stmt = $pdo->prepare('INSERT INTO critters (noun_id, adjective_id, verb_id, location_id, author_id, image) VALUES (:noun_id, :adjective_id, :verb_id, :location_id, :author_id, :image)');
|
|
$stmt->execute([
|
|
'noun_id' => $selected['noun_id'],
|
|
'adjective_id' => $selected['adjective_id'],
|
|
'verb_id' => $selected['verb_id'],
|
|
'location_id' => $selected['location_id'],
|
|
'author_id' => $_SESSION['user_id'],
|
|
'image' => $imageFilename,
|
|
]);
|
|
|
|
header('Location: /');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
function renderOptions(array $rows, $selectedId): void
|
|
{
|
|
foreach ($rows as $row) {
|
|
$isSelected = (string) $selectedId === (string) $row['id'];
|
|
echo '<option value="' . (int) $row['id'] . '"' . ($isSelected ? ' selected' : '') . '>'
|
|
. htmlspecialchars($row['value'], ENT_QUOTES, 'UTF-8')
|
|
. '</option>';
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Create Critter</title>
|
|
</head>
|
|
<body>
|
|
<h1>Create Your New Critter</h1>
|
|
<form action="create-critter.php" method="post" novalidate>
|
|
<div>
|
|
<label for="adjective_id">Adjective</label>
|
|
<select id="adjective_id" name="adjective_id">
|
|
<option value="">-- Select an adjective --</option>
|
|
<?php renderOptions($adjectives, $selected['adjective_id']); ?>
|
|
</select>
|
|
<?php if (isset($errors['adjective_id'])): ?>
|
|
<div><?php echo htmlspecialchars($errors['adjective_id'], ENT_QUOTES, 'UTF-8'); ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div>
|
|
<label for="noun_id">Noun</label>
|
|
<select id="noun_id" name="noun_id">
|
|
<option value="">-- Select a noun --</option>
|
|
<?php renderOptions($nouns, $selected['noun_id']); ?>
|
|
</select>
|
|
<?php if (isset($errors['noun_id'])): ?>
|
|
<div><?php echo htmlspecialchars($errors['noun_id'], ENT_QUOTES, 'UTF-8'); ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div>
|
|
<label for="verb_id">Verb</label>
|
|
<select id="verb_id" name="verb_id">
|
|
<option value="">-- Select a verb --</option>
|
|
<?php renderOptions($verbs, $selected['verb_id']); ?>
|
|
</select>
|
|
<?php if (isset($errors['verb_id'])): ?>
|
|
<div><?php echo htmlspecialchars($errors['verb_id'], ENT_QUOTES, 'UTF-8'); ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div>
|
|
<label for="location_id">Location</label>
|
|
<select id="location_id" name="location_id">
|
|
<option value="">-- Select a location --</option>
|
|
<?php renderOptions($locations, $selected['location_id']); ?>
|
|
</select>
|
|
<?php if (isset($errors['location_id'])): ?>
|
|
<div><?php echo htmlspecialchars($errors['location_id'], ENT_QUOTES, 'UTF-8'); ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<button type="submit">Create Critter</button>
|
|
</form>
|
|
</body>
|
|
</html>
|