Files
Critters/create-critter.php
T
pauljdandClaude Sonnet 5 aab614f811
pauljd/davidson-ci: Davidson CI / quality (push) Successful in 13s
pauljd/davidson-ci: Davidson CI / ai-review (push) Successful in 1m2s
Add auth, critter creation with AI image generation, and Tailwind styling
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>
2026-09-05 22:41:20 +01:00

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>