Adds server-side image safety check to ensure users upload pet photos instead of human images. Uses NudeNet with 0.5 confidence threshold and fail-open behavior. Frontend shows loading state during analysis and error UI when humans are detected. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
No EOL
11 KiB
PHP
179 lines
No EOL
11 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
|
<title>Pets at Home</title>
|
|
<meta name="description" content="Create a personalized love song for your pet with our fun and easy tool.">
|
|
<link rel="stylesheet" href="assets/css/style.css">
|
|
<link rel="stylesheet" href="https://unpkg.com/cropperjs@1.6.2/dist/cropper.min.css">
|
|
|
|
<?php include('opengraph.php'); ?>
|
|
</head>
|
|
<body x-data="petSongForm">
|
|
|
|
<?php include('header.php'); ?>
|
|
|
|
<div class="container">
|
|
|
|
<div class="body-container">
|
|
|
|
<img class="jukebox-banner-mb" src="assets/images/jukebox-banner-mb.png" alt="Jukebox" />
|
|
<img class="jukebox-banner-dt" src="assets/images/jukebox-banner-dt.png" alt="Jukebox" />
|
|
|
|
<div class="title">Let's make your <br/>Pet Love Song.</div>
|
|
<div class="sub-title">Tell us about your furry, finned, <br/>or feathered Valentine and our <br/>song-maker will do the rest.</div>
|
|
|
|
<form id="uploadForm" @submit.prevent="submitForm">
|
|
|
|
<div class="form-row">
|
|
|
|
<div class="form-group">
|
|
<label for="petName">Pet name</label>
|
|
<input type="text" id="petName" name="petName" x-model="formData.pet_name" pattern="[A-Za-z ]+" minlength="2" maxlength="100" oninput="this.value = this.value.replace(/[^A-Za-z ]/g, '');" required :class="{ 'input-error': profanityState.pet_name }">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="petType">Pet type</label>
|
|
<select id="petType" name="petType" class="form-control" x-model="formData.pet_type" required>
|
|
<option value=""></option>
|
|
<template x-for="type in petCategory" :key="type">
|
|
<option :value="type" x-text="type"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="file-upload">
|
|
<input type="file" id="image" x-ref="imageInput" accept="image/jpeg,image/jpg,image/png">
|
|
<label for="image" class="file-upload-label" id="upload-label" x-show="!isCropping && !formData.photo">
|
|
<div class="file-upload-text">Upload a photo or take a pic</div>
|
|
<div class="file-upload-subtext">(make sure it's clear and their face is front & centre)</div>
|
|
</label>
|
|
<!-- START -->
|
|
<!-- Cropper state (hidden initially) -->
|
|
<div id="cropper-box" class="file-upload-label" x-show="isCropping" style="display:none; padding: 10px;">
|
|
<div style="max-width: 100%; max-height: 300px; overflow: hidden;">
|
|
<img id="cropper-image" x-ref="cropperImg" src="" style="max-width: 100%;">
|
|
</div>
|
|
<div style="margin-top: 10px; display: flex; gap: 10px; justify-content: center;">
|
|
<button type="button" @click="acceptCrop()" id="btn-accept" style="background: #00AA28; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">✓ Accept</button>
|
|
<button type="button" @click="cancelCrop()" id="btn-cancel" style="background: #666; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">✕ Cancel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview state (hidden initially) -->
|
|
<div id="preview-box" @click="$refs.imageInput.click()" class="file-upload-label" x-show="formData.photo && !isCropping && !humanDetected" style="display:none; padding: 10px; cursor: pointer; position: relative;">
|
|
<img id="preview-image" src="" style="width: 100%; max-width: 300px; border-radius: 4px;">
|
|
<div style="margin-top: 10px; font-size: 14px;">Click to change photo</div>
|
|
<!-- Image check overlay -->
|
|
<div x-show="isCheckingImage" class="image-check-overlay" x-cloak>
|
|
<div class="image-check-spinner"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Human detection error state -->
|
|
<div id="human-detected-box" class="file-upload-label human-detected-error" x-show="humanDetected && !isCropping" style="display:none; padding: 20px;" x-cloak>
|
|
<div class="human-detected-icon">!</div>
|
|
<div class="human-detected-message">Oops! We detected a human in this photo. Please upload a photo of your pet.</div>
|
|
<button type="button" @click="retryImageUpload()" class="human-detected-retry-btn">Try again</button>
|
|
</div>
|
|
<!-- END -->
|
|
</div>
|
|
<input type="hidden" name="photo" id="photo" x-model="formData.photo">
|
|
|
|
<div class="form-group">
|
|
<label for="musicVibe">Music vibe?</label>
|
|
<select id="musicVibe" name="musicVibe" class="form-control" x-model="formData.music_vibe" required>
|
|
<option value=""></option>
|
|
<template x-for="genre in musicGenre" :key="genre">
|
|
<option :value="genre" x-text="genre"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="yourName">Your name</label>
|
|
<input type="text" id="yourName" name="yourName" x-model="formData.owner_name" pattern="[A-Za-z ]+" minlength="2" maxlength="100" oninput="this.value = this.value.replace(/[^A-Za-z ]/g, '');" required :class="{ 'input-error': profanityState.owner_name }">
|
|
</div>
|
|
|
|
<button type="submit" id="submit-btn" class="submit-btn" :disabled="isSubmitting || hasProfanity() || isCheckingImage" x-text="isSubmitting ? 'Creating...' : 'Create my Pet Love Song'">Create my Pet Love Song</button>
|
|
|
|
<div x-show="isCheckingImage" class="safety-check-message" x-cloak>
|
|
Analyzing image for safety - please wait
|
|
</div>
|
|
|
|
<div class="profanity-warning" x-show="hasProfanity()" x-cloak>
|
|
<span class="warning-icon">!</span>
|
|
<span class="warning-text">Please remove inappropriate language to continue.</span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
<div id="errorModal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="heart-icon">
|
|
<img class="white-heart-1" src="assets/images/heart-white.png" alt="White Heart" />
|
|
</div>
|
|
<div class="modal-header">We're all out of Pet Love Songs!</div>
|
|
<div class="modal-message" id="modalMessage">We use cookies to improve your experience and track usage. Choosing <b>Accept & Continue</b> means you agree to the storage of this metering cookie. Choosing <b>Reject All</b> means that you do not agree to the metering cookie, and we will not track your usage.</div>
|
|
<button class="modal-close" onclick="document.getElementById('errorModal').style.display='none'">Shop now</button>
|
|
<div class="heart-icon">
|
|
<img class="white-heart-2" src="assets/images/heart-white.png" alt="White Heart" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="genericErrorModal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="heart-icon">
|
|
<img class="white-heart-1" src="assets/images/heart-white.png" alt="White Heart" />
|
|
</div>
|
|
<div class="modal-header">Oops!</div>
|
|
<div class="modal-message">Something went wrong. Please try again.</div>
|
|
<button class="modal-close" @click="closeGenericErrorModal()">Try again</button>
|
|
<div class="heart-icon">
|
|
<img class="white-heart-2" src="assets/images/heart-white.png" alt="White Heart" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- To display cookie popup - display: flex -->
|
|
<div id="cookiePopup" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">Cookie Policy</div>
|
|
<div class="modal-message" id="modalMessage">We use cookies to improve your experience and track usage. Choosing Accept & Continue means you agree to the storage of this metering cookie. Choosing Reject All means that you do not agree to the metering cookie, and we will not track your usage.</div>
|
|
<button id="cookie-accept-btn" class="modal-close">Accept & Continue</button>
|
|
<button id="cookie-reject-btn" class="modal-close">Reject All</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<?php include('footer.php'); ?>
|
|
|
|
<style>
|
|
/* Make cropper container responsive */
|
|
#cropper-box .cropper-container {
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
/* Ensure file-upload-label works for all states */
|
|
.file-upload-label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
</style>
|
|
<script defer src="https://unpkg.com/cropperjs@1.6.2/dist/cropper.min.js"></script>
|
|
<script defer src="./assets/js/home.js"></script>
|
|
<script defer src="https://unpkg.com/alpinejs@3.15.5/dist/cdn.min.js"></script>
|
|
</body>
|
|
</html>
|