filename:
views/public/profile.php
branch:
main
back to repo
<?php
if (!isLoggedIn()) {
header('Location: ' . url('signin'));
exit;
}
$pageTitle = "Your profile – Seven O'Clock Dinner";
$errors = [];
$successMessage = null;
$userId = currentUserId();
// Build a shareable absolute URL to this member's profile card on the members page.
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$profilePath = url('members?user_id=' . $userId . '#member-' . $userId);
if (strpos($profilePath, 'http://') === 0 || strpos($profilePath, 'https://') === 0) {
$profileShareUrl = $profilePath;
} else {
$profileShareUrl = $scheme . '://' . $host . $profilePath;
}
// Fetch existing profile data
$stmt = $db->prepare('SELECT u.full_name, u.email, m.display_name, m.photo_url, m.short_bio, m.city, m.links_json FROM users u LEFT JOIN member_profiles m ON m.user_id = u.id WHERE u.id = ?');
$stmt->bind_param('i', $userId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if (!$row) {
http_response_code(404);
$errors[] = 'Profile not found.';
} else {
$displayName = $row['display_name'] ?: $row['full_name'];
$photoUrl = $row['photo_url'] ?? null;
$bio = $row['short_bio'] ?? '';
$city = $row['city'] ?? '';
$links = [];
if (!empty($row['links_json'])) {
$decoded = json_decode($row['links_json'], true);
if (is_array($decoded)) {
$links = $decoded;
}
}
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$displayName = trim($_POST['display_name'] ?? $displayName);
$city = trim($_POST['city'] ?? '');
$bio = trim($_POST['bio'] ?? '');
$rawLinks = $_POST['links'] ?? [];
if (!is_array($rawLinks)) {
$rawLinks = [$rawLinks];
}
$newLinks = [];
foreach ($rawLinks as $rawLink) {
$url = trim($rawLink ?? '');
if ($url === '') {
continue;
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = 'Please enter valid URLs for your links.';
break;
}
$newLinks[] = $url;
}
// Validate display name uniqueness
if ($displayName === '') {
$errors[] = 'Please enter a display name.';
} else {
$checkStmt = $db->prepare('SELECT 1 FROM member_profiles WHERE display_name = ? AND user_id <> ? LIMIT 1');
$checkStmt->bind_param('si', $displayName, $userId);
$checkStmt->execute();
$checkResult = $checkStmt->get_result();
if ($checkResult && $checkResult->num_rows > 0) {
$errors[] = 'That name is already taken. Please choose another.';
}
}
// Handle photo upload (optional)
if (!$errors && !empty($_FILES['photo']['name']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['photo'];
if ($file['size'] > 20 * 1024 * 1024) {
$errors[] = 'Profile photos are limited to 20MB.';
} else {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (in_array($mime, ['image/jpeg', 'image/png'], true)) {
$ext = $mime === 'image/png' ? 'png' : 'jpg';
$profilesDir = __DIR__ . '/../../uploads/profiles';
if (!is_dir($profilesDir)) {
mkdir($profilesDir, 0775, true);
}
$basename = bin2hex(random_bytes(12)) . '.' . $ext;
$target = $profilesDir . '/' . $basename;
if (move_uploaded_file($file['tmp_name'], $target)) {
$photoUrl = url('uploads/profiles/' . $basename);
} else {
$errors[] = 'We were unable to save your profile photo.';
}
} else {
$errors[] = 'Please upload a JPEG or PNG file for your profile photo.';
}
}
}
if (!$errors) {
$linksJson = $newLinks ? json_encode($newLinks, JSON_THROW_ON_ERROR) : null;
// Ensure a member_profiles row exists
if ($row['display_name'] === null && $row['photo_url'] === null && $row['short_bio'] === null && $row['city'] === null && $row['links_json'] === null) {
$stmt = $db->prepare('INSERT INTO member_profiles (user_id, display_name, photo_url, short_bio, city, links_json) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->bind_param('isssss', $userId, $displayName, $photoUrl, $bio, $city, $linksJson);
} else {
$stmt = $db->prepare('UPDATE member_profiles SET display_name = ?, photo_url = ?, short_bio = ?, city = ?, links_json = ? WHERE user_id = ?');
$stmt->bind_param('sssssi', $displayName, $photoUrl, $bio, $city, $linksJson, $userId);
}
$stmt->execute();
$successMessage = 'Your profile has been updated.';
$links = $newLinks;
}
}
}
?>
<section class="page-grid">
<div class="card" data-animate-initial style="position: relative;">
<a href="<?= url('logout') ?>" class="pill" style="position: absolute; top: 12px; right: 12px; font-size: 11px; padding: 5px 12px; white-space: nowrap;" onclick="return confirm('Are you sure you want to log out?');">
Log out
</a>
<div class="muted" style="font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 10px;">
Your profile
</div>
<h1 style="font-family: 'Georgia', 'Times New Roman', serif; font-weight: 400; font-size: 26px; margin: 0 0 12px;">
Everyone is going to see this.
</h1>
<?php if ($errors): ?>
<ul style="margin-top: 12px; padding-left: 18px; color: #9b2c2c; font-size: 13px;">
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err, ENT_QUOTES, 'UTF-8') ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($successMessage): ?>
<p style="margin-top: 12px; font-size: 13px; color: #22543d;">
<?= htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8') ?>
</p>
<?php endif; ?>
<div style="margin-top: 14px; text-align: center;">
<button
type="button"
class="pill"
style="font-size: 11px; padding: 6px 12px;"
onclick="(function(){try{var url='<?= htmlspecialchars($profileShareUrl, ENT_QUOTES, 'UTF-8') ?>';navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(url).then(function(){alert('Profile link copied to clipboard.');}).catch(function(){prompt('Copy this profile link:',url);}):prompt('Copy this profile link:',url);}catch(e){}})();"
>
Copy profile link
</button>
</div>
</div>
<?php if (!$row): ?>
<div class="card" data-animate>
<p class="muted" style="font-size: 13px;">
We could not find your profile.
</p>
</div>
<?php else: ?>
<div class="card" data-animate>
<form method="post" enctype="multipart/form-data" style="display: grid; gap: 10px; font-size: 13px;">
<label>
Name<br>
<input name="display_name" type="text" value="<?= htmlspecialchars($displayName, ENT_QUOTES, 'UTF-8') ?>" style="width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: rgba(255,255,255,0.8);">
</label>
<label>
Home Town (optional)<br>
<input name="city" type="text" value="<?= htmlspecialchars($city, ENT_QUOTES, 'UTF-8') ?>" style="width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: rgba(255,255,255,0.8);">
</label>
<label>
Links (optional)<br>
<div id="link-inputs" style="display: grid; gap: 6px; margin-top: 4px;">
<?php if ($links): ?>
<?php foreach ($links as $url): ?>
<div class="link-row" style="display: flex; gap: 6px;">
<input name="links[]" type="url" value="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>" style="flex: 1; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: rgba(255,255,255,0.8);">
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="link-row" style="display: flex; gap: 6px;">
<input name="links[]" type="url" placeholder="Website, portfolio, etc." style="flex: 1; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: rgba(255,255,255,0.8);">
</div>
<?php endif; ?>
</div>
<button type="button" id="add-link-btn" class="pill" style="margin-top: 6px; font-size: 11px; padding: 4px 10px;">
+ Add another link
</button>
</label>
<label>
Profile photo (optional)<br>
<?php if ($photoUrl): ?>
<div style="margin-bottom: 6px;">
<button type="button" data-image-full="<?= htmlspecialchars($photoUrl, ENT_QUOTES, 'UTF-8') ?>" style="all: unset; cursor: zoom-in;">
<img src="<?= htmlspecialchars($photoUrl, ENT_QUOTES, 'UTF-8') ?>" alt="Current profile photo" style="width: 64px; height: 64px; border-radius: 999px; object-fit: cover;">
</button>
</div>
<?php endif; ?>
<input name="photo" type="file" accept="image/jpeg,image/png" style="width: 100%; padding: 6px 0;">
</label>
<label>
Bio (optional)<br>
<textarea name="bio" rows="3" style="width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: rgba(255,255,255,0.8);"><?= htmlspecialchars($bio, ENT_QUOTES, 'UTF-8') ?></textarea>
</label>
<button type="submit" class="pill pill-accent" style="margin-top: 6px; justify-content: center;">
Save profile
</button>
</form>
<script>
(function () {
var container = document.getElementById('link-inputs');
var addBtn = document.getElementById('add-link-btn');
if (!container || !addBtn) return;
function createLinkRow() {
var row = document.createElement('div');
row.className = 'link-row';
row.style.display = 'flex';
row.style.gap = '6px';
var input = document.createElement('input');
input.type = 'url';
input.name = 'links[]';
input.placeholder = 'Another link (optional)';
input.style.flex = '1';
input.style.padding = '8px 10px';
input.style.borderRadius = '8px';
input.style.border = '1px solid rgba(0,0,0,0.12)';
input.style.background = 'rgba(255,255,255,0.8)';
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.textContent = '×';
removeBtn.title = 'Remove';
removeBtn.style.border = 'none';
removeBtn.style.background = 'transparent';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontSize = '16px';
removeBtn.style.lineHeight = '1';
removeBtn.style.padding = '0 4px';
removeBtn.style.color = 'rgba(0,0,0,0.45)';
removeBtn.addEventListener('mouseover', function () {
removeBtn.style.color = 'rgba(0,0,0,0.7)';
});
removeBtn.addEventListener('mouseout', function () {
removeBtn.style.color = 'rgba(0,0,0,0.45)';
});
removeBtn.addEventListener('click', function () {
container.removeChild(row);
});
row.appendChild(input);
row.appendChild(removeBtn);
return row;
}
addBtn.addEventListener('click', function () {
container.appendChild(createLinkRow());
});
})();
</script>
</div>
<?php endif; ?>
</section>