Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a39896c6c |
@ -1,8 +1,256 @@
|
|||||||
body {
|
/**
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
* Custom Styles for Compañía
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--brand-primary: #C05761; /* Deep Rose */
|
||||||
|
--brand-secondary: #E3C16F; /* Champagne Gold */
|
||||||
|
--brand-bg: #FDFBF7; /* Cream */
|
||||||
|
--brand-text: #333333; /* Charcoal */
|
||||||
|
--brand-text-muted: #6c757d;
|
||||||
|
--brand-white: #ffffff;
|
||||||
|
--brand-like: #28a745;
|
||||||
|
--brand-dislike: #dc3545;
|
||||||
|
|
||||||
|
--border-radius-lg: 16px;
|
||||||
|
--border-radius-md: 10px;
|
||||||
|
--font-serif: 'Playfair Display', serif;
|
||||||
|
--font-sans: 'Inter', sans-serif;
|
||||||
|
--shadow-soft: 0 4px 20px rgba(0,0,0,0.05);
|
||||||
|
--shadow-card: 0 10px 30px rgba(0,0,0,0.08);
|
||||||
|
--shadow-hover: 0 12px 35px rgba(192, 87, 97, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
body {
|
||||||
padding: 6rem 1rem;
|
background-color: var(--brand-bg);
|
||||||
background-color: #f8f9fa;
|
color: var(--brand-text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.03);
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--brand-primary) !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--brand-text) !important;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover, .nav-link.active {
|
||||||
|
color: var(--brand-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 50px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--brand-primary);
|
||||||
|
border-color: var(--brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #a6444d;
|
||||||
|
border-color: #a6444d;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 10px rgba(192, 87, 97, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: var(--brand-primary);
|
||||||
|
border-color: var(--brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover {
|
||||||
|
background-color: var(--brand-primary);
|
||||||
|
color: var(--brand-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Swiping UI --- */
|
||||||
|
#swipe-container {
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-swipe {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
background: var(--brand-white);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipe-left-anim {
|
||||||
|
from { transform: translateX(0) rotate(0); opacity: 1; }
|
||||||
|
to { transform: translateX(-150%) rotate(-20deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipe-right-anim {
|
||||||
|
from { transform: translateX(0) rotate(0); opacity: 1; }
|
||||||
|
to { transform: translateX(150%) rotate(20deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-left {
|
||||||
|
animation: swipe-left-anim 0.5s forwards ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-right {
|
||||||
|
animation: swipe-right-anim 0.5s forwards ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companion-card .card-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companion-img-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 100%;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-top-left-radius: var(--border-radius-lg);
|
||||||
|
border-top-right-radius: var(--border-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companion-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
color: var(--brand-primary);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.companion-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companion-meta {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--brand-text-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-badge {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--brand-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer-swipe {
|
||||||
|
border-top: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-swipe {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--brand-white);
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-swipe:hover {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dislike {
|
||||||
|
border-bottom-left-radius: var(--border-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dislike:hover {
|
||||||
|
color: var(--brand-dislike);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-like {
|
||||||
|
border-left: none;
|
||||||
|
border-bottom-right-radius: var(--border-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-like:hover {
|
||||||
|
color: var(--brand-like);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Match Notification Modal */
|
||||||
|
#matchNotificationModal .modal-content {
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
background: linear-gradient(135deg, var(--brand-primary), #a6444d);
|
||||||
|
color: var(--brand-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
#matchNotificationModal .modal-title {
|
||||||
|
color: var(--brand-white);
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#matchNotificationModal .btn-primary {
|
||||||
|
background-color: var(--brand-white);
|
||||||
|
color: var(--brand-primary);
|
||||||
|
border-color: var(--brand-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matches Modal */
|
||||||
|
#matchesModal .modal-content {
|
||||||
|
background-color: var(--brand-bg);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 2rem 0;
|
||||||
|
color: var(--brand-text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
5
assets/images/avatar_placeholder.svg
Normal file
5
assets/images/avatar_placeholder.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#f0f0f0">
|
||||||
|
<rect width="100" height="100" fill="#e0e0e0"/>
|
||||||
|
<circle cx="50" cy="40" r="20" fill="#bdbdbd"/>
|
||||||
|
<path d="M20 90 Q50 70 80 90 L80 100 L20 100 Z" fill="#bdbdbd"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
@ -0,0 +1,50 @@
|
|||||||
|
function handleSwipe(action, swiped_user_id) {
|
||||||
|
const card = document.querySelector('.card-swipe');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
// 1. Add class to trigger animation
|
||||||
|
const animationClass = action === 'like' ? 'swipe-right' : 'swipe-left';
|
||||||
|
card.classList.add(animationClass);
|
||||||
|
|
||||||
|
// 2. Wait for animation to end, then fetch
|
||||||
|
card.addEventListener('animationend', () => {
|
||||||
|
fetch('swipe.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: action,
|
||||||
|
swiped_user_id: swiped_user_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// 3. Replace the card with the next one
|
||||||
|
const swipeContainer = document.getElementById('swipe-container');
|
||||||
|
swipeContainer.innerHTML = data.next_companion_html;
|
||||||
|
|
||||||
|
// 4. If it's a match, show the notification modal
|
||||||
|
if (data.match) {
|
||||||
|
const matchModal = new bootstrap.Modal(document.getElementById('matchNotificationModal'));
|
||||||
|
const matchBody = document.getElementById('match-notification-body');
|
||||||
|
// Note: This relies on a global `translations` object or a dedicated function.
|
||||||
|
// For simplicity, we hardcode a message here, but a robust solution would use the `t()` function.
|
||||||
|
matchBody.textContent = `You and ${data.matched_user_name} have liked each other.`;
|
||||||
|
matchModal.show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle errors, e.g., show a message to the user
|
||||||
|
console.error('Swipe failed:', data.error);
|
||||||
|
// Re-enable card if swipe failed
|
||||||
|
card.classList.remove(animationClass);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during swipe:', error);
|
||||||
|
card.classList.remove(animationClass);
|
||||||
|
});
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
113
db/setup.php
Normal file
113
db/setup.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
// db/setup.php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
echo "Starting database setup...\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
// 1. Users Table
|
||||||
|
$sql_users = "CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('client', 'companion') NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)";
|
||||||
|
$pdo->exec($sql_users);
|
||||||
|
echo "- Table 'users' is ready.\n";
|
||||||
|
|
||||||
|
// 2. User Profiles Table
|
||||||
|
$sql_profiles = "CREATE TABLE IF NOT EXISTS user_profiles (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
age INT,
|
||||||
|
city VARCHAR(100),
|
||||||
|
bio TEXT,
|
||||||
|
profile_photo_path VARCHAR(255) DEFAULT 'assets/images/avatar_placeholder.svg',
|
||||||
|
hourly_rate DECIMAL(10, 2),
|
||||||
|
is_verified TINYINT(1) DEFAULT 0,
|
||||||
|
rating DECIMAL(3, 2) DEFAULT 5.00,
|
||||||
|
activities TEXT, -- Can store comma-separated values or JSON
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)";
|
||||||
|
$pdo->exec($sql_profiles);
|
||||||
|
echo "- Table 'user_profiles' is ready.\n";
|
||||||
|
|
||||||
|
// 3. Swipes Table
|
||||||
|
$sql_swipes = "CREATE TABLE IF NOT EXISTS swipes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
swiper_user_id INT NOT NULL,
|
||||||
|
swiped_user_id INT NOT NULL,
|
||||||
|
swipe_type ENUM('like', 'dislike') NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (swiper_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (swiped_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY (swiper_user_id, swiped_user_id)
|
||||||
|
)";
|
||||||
|
$pdo->exec($sql_swipes);
|
||||||
|
echo "- Table 'swipes' is ready.\n";
|
||||||
|
|
||||||
|
// 4. Matches Table
|
||||||
|
$sql_matches = "CREATE TABLE IF NOT EXISTS matches (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user1_id INT NOT NULL,
|
||||||
|
user2_id INT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user1_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user2_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY (user1_id, user2_id)
|
||||||
|
)";
|
||||||
|
$pdo->exec($sql_matches);
|
||||||
|
echo "- Table 'matches' is ready.\n";
|
||||||
|
|
||||||
|
|
||||||
|
// -- SEEDING DATA (if tables are empty) --
|
||||||
|
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
|
||||||
|
if ($stmt->fetchColumn() == 0) {
|
||||||
|
echo "Seeding initial data...\n";
|
||||||
|
|
||||||
|
// Hash for a default password, e.g., 'password123'
|
||||||
|
$default_password_hash = password_hash('password123', PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Companions
|
||||||
|
$companions_to_seed = [
|
||||||
|
[1, 'sofia@email.com', 'Sofia', 28, 'San José', 45.00, 'Art lover and coffee enthusiast. I know the best hidden cafes in Amón.', 1, 4.9, 'assets/images/avatar_placeholder.svg'],
|
||||||
|
[2, 'mateo@email.com', 'Mateo', 32, 'San José', 50.00, 'Local history buff. Let\'s walk through the city center.', 1, 4.8, 'assets/images/avatar_placeholder.svg'],
|
||||||
|
[3, 'elena@email.com', 'Elena', 25, 'Escazú', 60.00, 'Foodie and wine taster. Available for elegant dinners.', 1, 5.0, 'assets/images/avatar_placeholder.svg'],
|
||||||
|
[4, 'alejandro@email.com', 'Alejandro', 29, 'San Pedro', 40.00, 'Musician and movie buff. Great for concerts.', 0, 4.7, 'assets/images/avatar_placeholder.svg']
|
||||||
|
];
|
||||||
|
|
||||||
|
$user_sql = "INSERT INTO users (id, email, password_hash, role) VALUES (?, ?, ?, 'companion')";
|
||||||
|
$profile_sql = "INSERT INTO user_profiles (user_id, name, age, city, hourly_rate, bio, is_verified, rating, profile_photo_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
|
$user_stmt = $pdo->prepare($user_sql);
|
||||||
|
$profile_stmt = $pdo->prepare($profile_sql);
|
||||||
|
|
||||||
|
foreach ($companions_to_seed as $c) {
|
||||||
|
$user_stmt->execute([$c[0], $c[1], $default_password_hash]);
|
||||||
|
$profile_stmt->execute([$c[0], $c[2], $c[3], $c[4], $c[5], $c[6], $c[7], $c[8], $c[9]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A client user for testing
|
||||||
|
$client_user_sql = "INSERT INTO users (email, password_hash, role) VALUES ('client@email.com', ?, 'client')";
|
||||||
|
$pdo->prepare($client_user_sql)->execute([$default_password_hash]);
|
||||||
|
$client_user_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$client_profile_sql = "INSERT INTO user_profiles (user_id, name, age, city, bio) VALUES (?, 'Test Client', 30, 'San José', 'Looking for great company.')";
|
||||||
|
$pdo->prepare($client_profile_sql)->execute([$client_user_id]);
|
||||||
|
|
||||||
|
echo "- Seeded companion and client users.\n";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo "- Users table already has data. Skipping seed.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Database setup completed successfully!\n";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("DB Setup Error: " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
126
includes/helpers.php
Normal file
126
includes/helpers.php
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$translations = [
|
||||||
|
'en' => [
|
||||||
|
'page_title' => 'Compañía — Platonic Companion Booking',
|
||||||
|
'nav_companions' => 'Companions',
|
||||||
|
'nav_how_it_works' => 'How it Works',
|
||||||
|
'nav_matches' => 'My Matches',
|
||||||
|
'nav_logout' => 'Logout',
|
||||||
|
'hero_title_1' => 'Find your perfect',
|
||||||
|
'hero_title_2' => 'platonic',
|
||||||
|
'hero_title_3' => 'companion.',
|
||||||
|
'hero_lead' => 'Book vetted companions for dinner dates, movie nights, or city tours. Safe, secure, and strictly platonic.',
|
||||||
|
'search_where' => 'Where?',
|
||||||
|
'search_city_any' => 'Any City',
|
||||||
|
'search_activity' => 'Activity / Keyword',
|
||||||
|
'search_placeholder' => 'e.g. Dinner, Coffee, Casual Dates...',
|
||||||
|
'search_button' => 'Search Companions',
|
||||||
|
'featured_companions' => 'Discover Companions',
|
||||||
|
'no_companions_found' => 'No new companions found. Check back later!',
|
||||||
|
'clear_filters' => 'Clear Filters',
|
||||||
|
'verified' => 'Verified',
|
||||||
|
'rate_per_hour' => '/hr',
|
||||||
|
'like_button' => 'Like',
|
||||||
|
'dislike_button' => 'Dislike',
|
||||||
|
'matches_title' => 'Your Matches',
|
||||||
|
'matches_empty' => 'No matches yet. Keep swiping!',
|
||||||
|
'match_modal_title' => 'It\'s a Match!',
|
||||||
|
'match_modal_body' => 'You and {name} have liked each other.',
|
||||||
|
'match_modal_button' => 'Keep Swiping',
|
||||||
|
'footer_copyright' => 'Compañía. All rights reserved.',
|
||||||
|
'footer_coc' => 'Strict Code of Conduct: Sexual services are strictly prohibited.',
|
||||||
|
'footer_safety' => 'Read our Safety Guidelines',
|
||||||
|
],
|
||||||
|
'es' => [
|
||||||
|
'page_title' => 'Compañía — Reserva de Compañeros Platónicos',
|
||||||
|
'nav_companions' => 'Compañeros',
|
||||||
|
'nav_how_it_works' => 'Cómo Funciona',
|
||||||
|
'nav_matches' => 'Mis Matches',
|
||||||
|
'nav_logout' => 'Cerrar Sesión',
|
||||||
|
'hero_title_1' => 'Encuentra tu compañero',
|
||||||
|
'hero_title_2' => 'platónico',
|
||||||
|
'hero_title_3' => 'perfecto.',
|
||||||
|
'hero_lead' => 'Reserva compañeros verificados para cenas, noches de cine o tours por la ciudad. Seguro, protegido y estrictamente platónico.',
|
||||||
|
'search_where' => '¿Dónde?',
|
||||||
|
'search_city_any' => 'Cualquier ciudad',
|
||||||
|
'search_activity' => 'Actividad / Palabra Clave',
|
||||||
|
'search_placeholder' => 'Ej: Cena, Café, Citas Casuales...',
|
||||||
|
'search_button' => 'Buscar Compañeros',
|
||||||
|
'featured_companions' => 'Descubre Compañeros',
|
||||||
|
'no_companions_found' => 'No se encontraron nuevos compañeros. ¡Vuelve más tarde!',
|
||||||
|
'clear_filters' => 'Limpiar Filtros',
|
||||||
|
'verified' => 'Verificado',
|
||||||
|
'rate_per_hour' => '/hora',
|
||||||
|
'like_button' => 'Me gusta',
|
||||||
|
'dislike_button' => 'No me gusta',
|
||||||
|
'matches_title' => 'Tus Matches',
|
||||||
|
'matches_empty' => 'Aún no tienes matches. ¡Sigue deslizando!',
|
||||||
|
'match_modal_title' => '¡Es un Match!',
|
||||||
|
'match_modal_body' => 'A ti y a {name} se gustaron.',
|
||||||
|
'match_modal_button' => 'Seguir Deslizando',
|
||||||
|
'footer_copyright' => 'Compañía. Todos los derechos reservados.',
|
||||||
|
'footer_coc' => 'Código de Conducta Estricto: Los servicios sexuales están estrictamente prohibidos.',
|
||||||
|
'footer_safety' => 'Lee nuestras Guías de Seguridad',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
function t($key, $replacements = []) {
|
||||||
|
global $lang, $translations;
|
||||||
|
// Fallback to session language if not passed explicitly
|
||||||
|
$current_lang = $lang ?? $_SESSION['lang'] ?? 'en';
|
||||||
|
$text = $translations[$current_lang][$key] ?? $key;
|
||||||
|
foreach ($replacements as $k => $v) {
|
||||||
|
$text = str_replace('{'.$k.'}', $v, $text);
|
||||||
|
}
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_companion_card($companion) {
|
||||||
|
if (!$companion) {
|
||||||
|
return '<div class="text-center py-5"><p class="text-muted fs-5">' . t('no_companions_found') . '</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$companion['img'] = $companion['profile_photo_path'] ?? 'assets/images/avatar_placeholder.svg';
|
||||||
|
$verified_badge = $companion['is_verified'] ? '<div class="verified-badge"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-patch-check-fill" viewBox="0 0 16 16"><path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.896-.011a2.89 2.89 0 0 0-2.924 2.924l.01.896-.636.622a2.89 2.89 0 0 0 0 4.134l.638.622-.011.896a2.89 2.89 0 0 0 2.924 2.924l.896-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.638.896.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.896.636-.622a2.89 2.89 0 0 0 0-4.134l-.638-.622.011-.896a2.89 2.89 0 0 0-2.924-2.924l-.896.01-.622-.636zm.93 5.395l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.293l2.646-2.647a.5.5 0 0 1 .708.708z"/></svg> ' . t('verified') . '</div>' : '';
|
||||||
|
|
||||||
|
return '
|
||||||
|
<div class="card companion-card card-swipe" data-companion-id="'.$companion['user_id'].'">
|
||||||
|
<div class="companion-img-wrapper">
|
||||||
|
<img src="'.htmlspecialchars($companion['img']).'" alt="'.htmlspecialchars($companion['name']).'" class="companion-img">
|
||||||
|
'.$verified_badge.'
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="companion-name">'.htmlspecialchars($companion['name']).', '.$companion['age'].'</h3>
|
||||||
|
<div class="companion-meta">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></svg>
|
||||||
|
'.htmlspecialchars($companion['city']).'
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="rate-badge">'.number_format((float)($companion['hourly_rate'] ?? 0), 0).t('rate_per_hour').'</div>
|
||||||
|
<small class="text-muted">★ '.$companion['rating'].'</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="companion-bio mt-2">'.htmlspecialchars($companion['bio']).'</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-swipe">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col">
|
||||||
|
<button class="btn btn-swipe btn-dislike" onclick="handleSwipe(\'dislike\', '.$companion['user_id'].')">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16"><path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button class="btn btn-swipe btn-like" onclick="handleSwipe(\'like\', '.$companion['user_id'].')">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-heart-fill" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
304
index.php
304
index.php
@ -1,150 +1,188 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// index.php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
@ini_set('display_errors', '1');
|
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
// --- SESSION & AUTHENTICATION ---
|
||||||
$now = date('Y-m-d H:i:s');
|
session_start();
|
||||||
|
|
||||||
|
// If the user is not logged in, redirect to the login page.
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once 'includes/helpers.php';
|
||||||
|
|
||||||
|
// --- LANGUAGE & TRANSLATION ---
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
if (isset($_GET['lang']) && in_array($_GET['lang'], ['en', 'es'])) {
|
||||||
|
$lang = $_GET['lang'];
|
||||||
|
$_SESSION['lang'] = $lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$current_user_id = $_SESSION['user_id'];
|
||||||
|
$filterCity = $_GET['city'] ?? '';
|
||||||
|
$filterActivity = $_GET['activity'] ?? '';
|
||||||
|
|
||||||
|
// Fetch one companion that the user has not yet swiped on
|
||||||
|
$sql = "
|
||||||
|
SELECT p.*
|
||||||
|
FROM user_profiles p
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
WHERE u.role = 'companion'
|
||||||
|
AND p.user_id != :current_user_id
|
||||||
|
AND p.user_id NOT IN (
|
||||||
|
SELECT swiped_user_id FROM swipes WHERE swiper_user_id = :current_user_id
|
||||||
|
)
|
||||||
|
";
|
||||||
|
$params = [':current_user_id' => $current_user_id];
|
||||||
|
|
||||||
|
if ($filterCity) {
|
||||||
|
$sql .= " AND p.city LIKE :city";
|
||||||
|
$params[':city'] = '%' . $filterCity . '%';
|
||||||
|
}
|
||||||
|
if ($filterActivity) {
|
||||||
|
$sql .= " AND p.bio LIKE :activity";
|
||||||
|
$params[':activity'] = '%' . $filterActivity . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY RAND() LIMIT 1";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$companion = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Fetch matches for the logged-in user
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT p.*
|
||||||
|
FROM matches m
|
||||||
|
JOIN user_profiles p ON (m.user1_id = p.user_id OR m.user2_id = p.user_id)
|
||||||
|
WHERE (m.user1_id = :current_user_id OR m.user2_id = :current_user_id)
|
||||||
|
AND p.user_id != :current_user_id
|
||||||
|
");
|
||||||
|
$stmt->execute(['current_user_id' => $current_user_id]);
|
||||||
|
$matches = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($matches as &$m) {
|
||||||
|
$m['img'] = $m['profile_photo_path'] ?? 'assets/images/avatar_placeholder.svg';
|
||||||
|
}
|
||||||
|
unset($m);
|
||||||
|
|
||||||
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Find your perfect vetted companion for platonic experiences.';
|
||||||
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="<?= $lang ?>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>New Style</title>
|
<title><?= t('page_title') ?></title>
|
||||||
<?php
|
|
||||||
// Read project preview data from environment
|
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<?php if ($projectImageUrl): ?>
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
|
||||||
?>
|
<?php endif; ?>
|
||||||
<?php if ($projectDescription): ?>
|
|
||||||
<!-- Meta description -->
|
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
|
||||||
<!-- Open Graph meta tags -->
|
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
||||||
<!-- Twitter meta tags -->
|
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($projectImageUrl): ?>
|
|
||||||
<!-- Open Graph image -->
|
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
|
||||||
<!-- Twitter image -->
|
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
|
||||||
<?php endif; ?>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
|
||||||
:root {
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
--bg-color-start: #6a11cb;
|
<link href="assets/css/custom.css?v=<?= time() ?>" rel="stylesheet">
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% { background-position: 0% 0%; }
|
|
||||||
100% { background-position: 100% 100%; }
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
margin: 1.25rem auto 1.25rem;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px; height: 1px;
|
|
||||||
padding: 0; margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap; border: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
|
||||||
<div class="card">
|
<nav class="navbar navbar-expand-lg fixed-top">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<div class="container">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<a class="navbar-brand" href="index.php">Compañía</a>
|
||||||
<span class="sr-only">Loading…</span>
|
<div class="d-flex ms-auto align-items-center order-lg-2">
|
||||||
|
<div class="dropdown me-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" id="langDropdown" data-bs-toggle="dropdown" aria-expanded="false"><?= strtoupper($lang) ?></button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="langDropdown">
|
||||||
|
<li><a class="dropdown-item" href="?lang=en">EN (English)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="?lang=es">ES (Español)</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ul class="navbar-nav flex-row">
|
||||||
|
<li class="nav-item ms-2"><a href="logout.php" class="btn btn-outline-primary btn-sm"><?= t('nav_logout') ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"><span class="navbar-toggler-icon"></span></button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#"><?= t('nav_companions') ?></a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#"><?= t('nav_how_it_works') ?></a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#matchesModal"><?= t('nav_matches') ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Swiping UI -->
|
||||||
|
<main class="py-5 bg-light">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h2 class="h3 mb-4"><?= t('featured_companions') ?></h2>
|
||||||
|
|
||||||
|
<div id="swipe-container" class="mx-auto" style="max-width: 400px; position: relative; height: 650px;">
|
||||||
|
<?= render_companion_card($companion) ?>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Matches Modal -->
|
||||||
|
<div class="modal fade" id="matchesModal" tabindex="-1" aria-labelledby="matchesModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header"><h5 class="modal-title" id="matchesModalLabel"><?= t('matches_title') ?></h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<?php if (empty($matches)): ?>
|
||||||
|
<p class="text-center text-muted py-5"><?= t('matches_empty') ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||||
|
<?php foreach ($matches as $match): ?>
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100 text-center">
|
||||||
|
<img src="<?= htmlspecialchars($match['img']) ?>" class="card-img-top" alt="<?= htmlspecialchars($match['name']) ?>" style="height: 200px; object-fit: cover;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><?= htmlspecialchars($match['name']) ?></h5>
|
||||||
|
<button class="btn btn-primary btn-sm">Message</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Match Notification Modal -->
|
||||||
|
<div class="modal fade" id="matchNotificationModal" tabindex="-1" aria-labelledby="matchNotificationModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content text-center">
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<h2 class="modal-title" id="matchNotificationModalLabel"><?= t('match_modal_title') ?></h2>
|
||||||
|
<p id="match-notification-body" class="my-3"></p>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><?= t('match_modal_button') ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<div class="container text-center py-4">
|
||||||
|
<p>© <?= date('Y') ?> <?= t('footer_copyright') ?></p>
|
||||||
|
<p class="small text-muted"><?= t('footer_coc') ?> <a href="#"><?= t('footer_safety') ?></a>.</p>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
67
login.php
Normal file
67
login.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = $_POST['email'];
|
||||||
|
$password = $_POST['password'];
|
||||||
|
|
||||||
|
if (empty($email) || empty($password)) {
|
||||||
|
$error = 'Please fill in all fields.';
|
||||||
|
} else {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['user_role'] = $user['role'];
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = 'Invalid email or password.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container d-flex justify-content-center align-items-center vh-100">
|
||||||
|
<div class="card shadow-lg" style="width: 25rem;">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h1 class="card-title text-center mb-4">Login</h1>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form action="login.php" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p>Don't have an account? <a href="register.php">Register here</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
logout.php
Normal file
6
logout.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
93
register.php
Normal file
93
register.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = $_POST['email'];
|
||||||
|
$password = $_POST['password'];
|
||||||
|
$confirm_password = $_POST['confirm_password'];
|
||||||
|
$role = $_POST['role'];
|
||||||
|
|
||||||
|
if (empty($email) || empty($password) || empty($role)) {
|
||||||
|
$error = 'Please fill in all fields.';
|
||||||
|
} elseif ($password !== $confirm_password) {
|
||||||
|
$error = 'Passwords do not match.';
|
||||||
|
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$error = 'Invalid email format.';
|
||||||
|
} else {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$error = 'An account with this email already exists.';
|
||||||
|
} else {
|
||||||
|
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$pdo->prepare('INSERT INTO users (email, password_hash, role) VALUES (?, ?, ?)')
|
||||||
|
->execute([$email, $password_hash, $role]);
|
||||||
|
|
||||||
|
$user_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Create a basic profile
|
||||||
|
$pdo->prepare('INSERT INTO user_profiles (user_id, name, bio) VALUES (?, ?, ?)')
|
||||||
|
->execute([$user_id, 'New User', 'Please update your bio.']);
|
||||||
|
|
||||||
|
$success = 'Registration successful! You can now <a href="login.php">log in</a>.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Register</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container d-flex justify-content-center align-items-center vh-100">
|
||||||
|
<div class="card shadow-lg" style="width: 25rem;">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h1 class="card-title text-center mb-4">Register</h1>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?php echo $success; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form action="register.php" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">Confirm Password</label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="role" class="form-label">I am a...</label>
|
||||||
|
<select class="form-select" id="role" name="role">
|
||||||
|
<option value="client">Client (Looking for a companion)</option>
|
||||||
|
<option value="companion">Companion (Offering services)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">Register</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p>Already have an account? <a href="login.php">Login here</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
109
swipe.php
Normal file
109
swipe.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'includes/helpers.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$response = ['success' => false, 'match' => false, 'error' => null];
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
$response['error'] = 'User not authenticated.';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_user_id = $_SESSION['user_id'];
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$swiped_user_id = $data['swiped_user_id'] ?? null;
|
||||||
|
$action = $data['action'] ?? null; // 'like' or 'dislike'
|
||||||
|
|
||||||
|
if (!$swiped_user_id || !in_array($action, ['like', 'dislike'])) {
|
||||||
|
$response['error'] = 'Invalid input.';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// 1. Record the swipe action
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO swipes (swiper_user_id, swiped_user_id, swipe_type)
|
||||||
|
VALUES (:swiper_user_id, :swiped_user_id, :swipe_type)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':swiper_user_id' => $current_user_id,
|
||||||
|
':swiped_user_id' => $swiped_user_id,
|
||||||
|
':swipe_type' => $action
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. If it was a 'like', check for a mutual match
|
||||||
|
if ($action === 'like') {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*) FROM swipes
|
||||||
|
WHERE swiper_user_id = :swiped_user_id AND swiped_user_id = :current_user_id AND swipe_type = 'like'"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':swiped_user_id' => $swiped_user_id,
|
||||||
|
':current_user_id' => $current_user_id
|
||||||
|
]);
|
||||||
|
$is_mutual = (int)$stmt->fetchColumn() > 0;
|
||||||
|
|
||||||
|
if ($is_mutual) {
|
||||||
|
// 3. It's a match! Insert into matches table.
|
||||||
|
$user1 = min($current_user_id, $swiped_user_id);
|
||||||
|
$user2 = max($current_user_id, $swiped_user_id);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO matches (user1_id, user2_id)
|
||||||
|
SELECT :user1_id, :user2_id
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM matches
|
||||||
|
WHERE (user1_id = :user1_id AND user2_id = :user2_id) OR (user1_id = :user2_id AND user2_id = :user1_id)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':user1_id' => $user1,
|
||||||
|
':user2_id' => $user2
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response['match'] = true;
|
||||||
|
|
||||||
|
// Fetch the matched user's name for the notification
|
||||||
|
$stmt = $pdo->prepare("SELECT name FROM user_profiles WHERE user_id = :swiped_user_id");
|
||||||
|
$stmt->execute(['swiped_user_id' => $swiped_user_id]);
|
||||||
|
$matched_user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$response['matched_user_name'] = $matched_user['name'] ?? 'Someone';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$response['error'] = "Database error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, get the next companion to show
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT p.*
|
||||||
|
FROM user_profiles p
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
WHERE u.role = 'companion'
|
||||||
|
AND p.user_id != :current_user_id
|
||||||
|
AND p.user_id NOT IN (
|
||||||
|
SELECT swiped_user_id FROM swipes WHERE swiper_user_id = :current_user_id
|
||||||
|
)
|
||||||
|
ORDER BY RAND()
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
$stmt->execute(['current_user_id' => $current_user_id]);
|
||||||
|
$next_companion = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$response['next_companion_html'] = render_companion_card($next_companion);
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
Loading…
x
Reference in New Issue
Block a user