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 {
|
||||
padding: 6rem 1rem;
|
||||
background-color: #f8f9fa;
|
||||
body {
|
||||
background-color: var(--brand-bg);
|
||||
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>';
|
||||
}
|
||||
|
||||
318
index.php
318
index.php
@ -1,150 +1,188 @@
|
||||
<?php
|
||||
// index.php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
// --- SESSION & AUTHENTICATION ---
|
||||
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>
|
||||
<html lang="en">
|
||||
<html lang="<?= $lang ?>">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?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.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--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>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= t('page_title') ?></title>
|
||||
|
||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<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=Inter:wght@400;500;600&family=Playfair+Display:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="assets/css/custom.css?v=<?= time() ?>" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</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>
|
||||
|
||||
<nav class="navbar navbar-expand-lg fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="index.php">Compañía</a>
|
||||
<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>
|
||||
</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>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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