|
|
|
@@ -55,6 +55,8 @@
|
|
|
|
|
.btn-primary:hover:not(:disabled) { opacity: 0.9; }
|
|
|
|
|
.btn-ghost { background: transparent; color: var(--muted); border: 1.5px solid var(--border); }
|
|
|
|
|
.btn-ghost:hover { border-color: var(--teal); color: var(--teal); }
|
|
|
|
|
.btn-google { background: #fff; color: #3c4043; border: 1.5px solid #dadce0; font-family: var(--font-body); font-size: 15px; font-weight: 500; width: 100%; margin-bottom: 4px; gap: 10px; }
|
|
|
|
|
.btn-google:hover { background: #f8f9fa; border-color: #c6c6c6; }
|
|
|
|
|
.btn-danger { background: transparent; color: var(--danger); border: 1.5px solid var(--danger); }
|
|
|
|
|
.btn-danger:hover { background: rgba(255,107,107,0.1); }
|
|
|
|
|
.btn-sm { padding: 7px 14px; font-size: 13px; min-height: 38px; }
|
|
|
|
@@ -266,7 +268,16 @@
|
|
|
|
|
<p class="auth-tagline">Walk more. Compete with friends.</p>
|
|
|
|
|
<div class="auth-card" id="auth-card">
|
|
|
|
|
<h2>Welcome</h2>
|
|
|
|
|
<p>Enter your email to sign in or create an account — no password needed.</p>
|
|
|
|
|
<p>Sign in or create an account to get started.</p>
|
|
|
|
|
<button class="btn btn-google" id="google-signin-btn" onclick="signInWithGoogle()" style="display:none">
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 18 18" style="flex-shrink:0"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.184l-2.908-2.258c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z"/><path fill="#FBBC05" d="M3.964 10.707c-.18-.54-.282-1.117-.282-1.707s.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.55 0 9s.348 2.825.957 4.039l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.961L3.964 7.293C4.672 5.166 6.656 3.58 9 3.58z"/></svg>
|
|
|
|
|
Sign in with Google
|
|
|
|
|
</button>
|
|
|
|
|
<div id="google-divider" style="display:none;align-items:center;gap:12px;margin:16px 0">
|
|
|
|
|
<div style="flex:1;height:1px;background:var(--border)"></div>
|
|
|
|
|
<span style="font-size:12px;color:var(--muted)">or</span>
|
|
|
|
|
<div style="flex:1;height:1px;background:var(--border)"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<label for="auth-email">Email address</label>
|
|
|
|
|
<input type="email" id="auth-email" placeholder="you@example.com" autocomplete="email">
|
|
|
|
|
<div class="error-msg" id="auth-error" style="display:none"></div>
|
|
|
|
@@ -332,8 +343,8 @@
|
|
|
|
|
<div class="bc-icon">🍎</div><div class="bc-name">Health Auto Export</div>
|
|
|
|
|
<div class="bc-platform">iPhone · iOS 16+</div><div class="bc-badge">Free tier</div>
|
|
|
|
|
</a>
|
|
|
|
|
<a href="https://github.com/mcnaveen/health-connect-webhook/releases" target="_blank" class="bridge-card">
|
|
|
|
|
<div class="bc-icon">🤖</div><div class="bc-name">HC Webhook</div>
|
|
|
|
|
<a href="https://github.com/JustinOros/stepmates-sync/releases/download/v1.1/StepMates-Sync-Android.apk" target="_blank" class="bridge-card">
|
|
|
|
|
<div class="bc-icon">🤖</div><div class="bc-name">StepMates Sync</div>
|
|
|
|
|
<div class="bc-platform">Android 9+</div><div class="bc-badge">Free & open source</div>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
@@ -355,8 +366,8 @@
|
|
|
|
|
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;font-size:13px;color:var(--muted);line-height:1.8;margin-bottom:16px">
|
|
|
|
|
<strong style="color:var(--white)">iPhone (Health Auto Export):</strong><br>
|
|
|
|
|
Automations → New Automation → Webhook → paste URL → select Steps → set schedule Daily<br><br>
|
|
|
|
|
<strong style="color:var(--white)">Android (HC Webhook):</strong><br>
|
|
|
|
|
Add Webhook → paste URL → enable Steps → set sync interval
|
|
|
|
|
<strong style="color:var(--white)">Android (StepMates Sync):</strong><br>
|
|
|
|
|
Download and install the app → paste your Webhook URL → tap Sync
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn btn-primary" onclick="finishOnboarding()">Go to my dashboard →</button>
|
|
|
|
|
</div>
|
|
|
|
@@ -467,19 +478,21 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;font-size:13px;color:var(--muted);line-height:1.9;margin-bottom:28px">
|
|
|
|
|
<strong style="color:var(--white)">iPhone — Health Auto Export:</strong><br>
|
|
|
|
|
Automations → New Automation → Webhook → paste URL<br>
|
|
|
|
|
Select <em>Steps</em> → set to run Daily<br><br>
|
|
|
|
|
<strong style="color:var(--white)">Android — HC Webhook:</strong><br>
|
|
|
|
|
<a href="https://github.com/mcnaveen/health-connect-webhook/releases" target="_blank" style="color:var(--teal)">Download from GitHub</a> → Add Webhook → paste URL<br>
|
|
|
|
|
Enable Steps → set sync interval
|
|
|
|
|
1. Download <a href="https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069" target="_blank" style="color:var(--teal)">Health Auto Export</a> from the App Store<br>
|
|
|
|
|
2. Automations → New Automation → Webhook → paste your URL above<br>
|
|
|
|
|
3. Select <em>Steps</em> → set Date Range to <em>Default</em> → set schedule to <em>Daily</em><br><br>
|
|
|
|
|
<strong style="color:var(--white)">Android — StepMates Sync App:</strong><br>
|
|
|
|
|
1. Download <a href="https://github.com/JustinOros/stepmates-sync/releases/download/v1.1/StepMates-Sync-Android.apk" target="_blank" style="color:var(--teal)">StepMates Sync Android App</a><br>
|
|
|
|
|
2. Install it (allow unknown sources when prompted)<br>
|
|
|
|
|
3. Paste your Webhook URL above into the app → tap Sync
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-header"><div class="section-title">Bridge App Downloads</div></div>
|
|
|
|
|
<div class="bridge-cards" style="margin-bottom:32px">
|
|
|
|
|
<a href="https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069" target="_blank" class="bridge-card">
|
|
|
|
|
<div class="bc-icon">🍎</div><div class="bc-name">Health Auto Export</div><div class="bc-platform">iPhone · iOS 16+</div><div class="bc-badge">Free tier</div>
|
|
|
|
|
</a>
|
|
|
|
|
<a href="https://github.com/mcnaveen/health-connect-webhook/releases" target="_blank" class="bridge-card">
|
|
|
|
|
<div class="bc-icon">🤖</div><div class="bc-name">HC Webhook</div><div class="bc-platform">Android 9+</div><div class="bc-badge">Free & open source</div>
|
|
|
|
|
<a href="https://github.com/JustinOros/stepmates-sync/releases/download/v1.1/StepMates-Sync-Android.apk" target="_blank" class="bridge-card">
|
|
|
|
|
<div class="bc-icon">🤖</div><div class="bc-name">StepMates Sync</div><div class="bc-platform">Android 9+</div><div class="bc-badge">Free & open source</div>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-header"><div class="section-title">Account</div></div>
|
|
|
|
@@ -516,10 +529,14 @@
|
|
|
|
|
<span style="color:var(--muted);font-size:14px">Display Name</span>
|
|
|
|
|
<span id="fp-name-text" onclick="openNicknameModal()" style="font-size:14px;font-weight:600;cursor:pointer;color:var(--teal);text-decoration:underline;text-underline-offset:3px;text-decoration-style:dotted">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;justify-content:space-between;padding:14px 18px">
|
|
|
|
|
<div style="display:flex;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border)">
|
|
|
|
|
<span style="color:var(--muted);font-size:14px">Date Joined</span>
|
|
|
|
|
<span style="font-size:14px;font-weight:500" id="fp-joined">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;justify-content:space-between;padding:14px 18px">
|
|
|
|
|
<span style="color:var(--muted);font-size:14px">Last Synced</span>
|
|
|
|
|
<span style="font-size:14px;font-weight:500" id="fp-last-synced">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;gap:12px">
|
|
|
|
@@ -577,8 +594,12 @@
|
|
|
|
|
</div>
|
|
|
|
|
<label for="profile-name-input">Display name</label>
|
|
|
|
|
<input type="text" id="profile-name-input" placeholder="e.g. Alex or RunnerAlex" maxlength="30">
|
|
|
|
|
<label for="profile-avatar-input">Avatar image URL <span style="font-weight:400;text-transform:none;font-size:11px;color:var(--muted)">(optional)</span></label>
|
|
|
|
|
<input type="text" id="profile-avatar-input" placeholder="https://example.com/photo.jpg">
|
|
|
|
|
<label>Avatar Emoji</label>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:4px">
|
|
|
|
|
<div id="emoji-preview" style="width:48px;height:48px;border-radius:50%;background:var(--surface-2);border:2px solid var(--teal);display:flex;align-items:center;justify-content:center;font-size:26px;flex-shrink:0">🏃</div>
|
|
|
|
|
<input type="text" id="profile-avatar-input" placeholder="Click here, then press the emoji key (⊞+. on Windows, Ctrl+Cmd+Space on Mac, or emoji keyboard on mobile)" style="flex:1;font-size:24px;text-align:center" oninput="updateEmojiPreview(this.value)">
|
|
|
|
|
</div>
|
|
|
|
|
<div style="font-size:12px;color:var(--muted);margin-bottom:8px">Mac: <strong>Ctrl+Cmd+Space</strong> · Windows: <strong>Win+.</strong> · Mobile: switch to emoji keyboard</div>
|
|
|
|
|
<div class="error-msg" id="profile-error" style="display:none"></div>
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button class="btn btn-ghost" onclick="closeProfileModal()">Cancel</button>
|
|
|
|
@@ -620,7 +641,7 @@
|
|
|
|
|
<script src="config.js"></script>
|
|
|
|
|
<script type="module">
|
|
|
|
|
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js';
|
|
|
|
|
import { getAuth, isSignInWithEmailLink, signInWithEmailLink, signOut as fbSignOut, onAuthStateChanged } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-auth.js';
|
|
|
|
|
import { getAuth, isSignInWithEmailLink, signInWithEmailLink, signInWithPopup, signInWithRedirect, getRedirectResult, GoogleAuthProvider, signOut as fbSignOut, onAuthStateChanged } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-auth.js';
|
|
|
|
|
import { getFirestore, doc, getDoc, setDoc, updateDoc, deleteField, collection, query, where, getDocs, addDoc, onSnapshot, serverTimestamp } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js';
|
|
|
|
|
|
|
|
|
|
if (!window.STEPMATES_CONFIG) {
|
|
|
|
@@ -636,6 +657,12 @@ const fbApp = initializeApp(FIREBASE_CONFIG);
|
|
|
|
|
const auth = getAuth(fbApp);
|
|
|
|
|
const db = getFirestore(fbApp);
|
|
|
|
|
|
|
|
|
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
|
|
|
if (!isIOS) {
|
|
|
|
|
document.getElementById('google-signin-btn').style.display = 'flex';
|
|
|
|
|
document.getElementById('google-divider').style.display = 'flex';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let currentUser = null;
|
|
|
|
|
let currentUserData = null;
|
|
|
|
|
let currentProfileData = null;
|
|
|
|
@@ -697,12 +724,19 @@ function todayKey() {
|
|
|
|
|
|
|
|
|
|
function esc(str) { return (str||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
|
|
|
|
|
|
|
|
function isEmoji(str) {
|
|
|
|
|
if (!str) return false;
|
|
|
|
|
const cp = str.codePointAt(0);
|
|
|
|
|
return (cp >= 0x1F300 && cp <= 0x1FAFF) || (cp >= 0x2600 && cp <= 0x27BF) || (cp >= 0xFE00 && cp <= 0xFE0F);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function avatarHtml(profile, size=40, nickname='') {
|
|
|
|
|
const displayName = profile?.displayName || 'No Display Name';
|
|
|
|
|
const initial = (nickname || displayName)[0].toUpperCase();
|
|
|
|
|
const url = profile?.avatarUrl || '';
|
|
|
|
|
if (url) return `<div class="lb-avatar" style="width:${size}px;height:${size}px"><img src="${esc(url)}" alt="${initial}" onerror="this.parentElement.innerHTML='${initial}'"></div>`;
|
|
|
|
|
return `<div class="lb-avatar" style="width:${size}px;height:${size}px">${initial}</div>`;
|
|
|
|
|
if (url && isEmoji(url)) return `<div class="lb-avatar" style="width:${size}px;height:${size}px;font-size:${Math.round(size*0.55)}px">${url}</div>`;
|
|
|
|
|
if (url && url.startsWith('http')) return `<div class="lb-avatar" style="width:${size}px;height:${size}px"><img src="${esc(url)}" alt="${initial}" onerror="this.parentElement.innerHTML='🏃'"></div>`;
|
|
|
|
|
return `<div class="lb-avatar" style="width:${size}px;height:${size}px;font-size:${Math.round(size*0.55)}px">🏃</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getFriendDisplayName(uid) {
|
|
|
|
@@ -711,8 +745,12 @@ function getFriendDisplayName(uid) {
|
|
|
|
|
return nick || displayName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _initializingUser = false;
|
|
|
|
|
|
|
|
|
|
// AUTH
|
|
|
|
|
onAuthStateChanged(auth, async (user) => {
|
|
|
|
|
if (_initializingUser) return;
|
|
|
|
|
|
|
|
|
|
if (isSignInWithEmailLink(auth, window.location.href)) {
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const email = urlParams.get('email') || localStorage.getItem('emailForSignIn');
|
|
|
|
@@ -755,6 +793,7 @@ window.confirmEmailSignIn = async function() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function initUser(user) {
|
|
|
|
|
_initializingUser = true;
|
|
|
|
|
currentUser = user;
|
|
|
|
|
const userRef = doc(db, 'users', user.uid);
|
|
|
|
|
const snap = await getDoc(userRef);
|
|
|
|
@@ -764,42 +803,35 @@ async function initUser(user) {
|
|
|
|
|
await setDoc(userRef, {
|
|
|
|
|
email: user.email,
|
|
|
|
|
webhookToken,
|
|
|
|
|
approved: false,
|
|
|
|
|
approved: true,
|
|
|
|
|
dailyGoal: 5000,
|
|
|
|
|
createdAt: serverTimestamp()
|
|
|
|
|
});
|
|
|
|
|
await setDoc(doc(db, 'profiles', user.uid), { displayName: '', avatarUrl: '' });
|
|
|
|
|
await setDoc(doc(db, 'profiles', user.uid), { displayName: user.displayName || '', avatarUrl: '🏃' });
|
|
|
|
|
await setDoc(doc(db, 'steps', user.uid), { stepsByDate: {} });
|
|
|
|
|
currentUserData = { email: user.email, webhookToken, approved: false, dailyGoal: 5000 };
|
|
|
|
|
await notifyAdminOfNewUser(user.email, user.uid);
|
|
|
|
|
showScreen('pending');
|
|
|
|
|
currentUserData = { email: user.email, webhookToken, approved: true, dailyGoal: 5000 };
|
|
|
|
|
const profileSnap = await getDoc(doc(db, 'profiles', user.uid));
|
|
|
|
|
currentProfileData = profileSnap.exists() ? profileSnap.data() : {};
|
|
|
|
|
currentStepsData = {};
|
|
|
|
|
loadDashboard();
|
|
|
|
|
} else {
|
|
|
|
|
currentUserData = snap.data();
|
|
|
|
|
if (currentUserData.approved === false) {
|
|
|
|
|
showScreen('pending');
|
|
|
|
|
} else if (!currentUserData.setupComplete) {
|
|
|
|
|
const profileSnap = await getDoc(doc(db, 'profiles', user.uid));
|
|
|
|
|
currentProfileData = profileSnap.exists() ? profileSnap.data() : {};
|
|
|
|
|
const stepsSnap = await getDoc(doc(db, 'steps', user.uid));
|
|
|
|
|
currentStepsData = stepsSnap.exists() ? stepsSnap.data().stepsByDate || {} : {};
|
|
|
|
|
showScreen('onboarding');
|
|
|
|
|
document.getElementById('ob-webhook-url').textContent = buildWebhookUrl(currentUserData.webhookToken);
|
|
|
|
|
} else {
|
|
|
|
|
const profileSnap = await getDoc(doc(db, 'profiles', user.uid));
|
|
|
|
|
currentProfileData = profileSnap.exists() ? profileSnap.data() : {};
|
|
|
|
|
const stepsSnap = await getDoc(doc(db, 'steps', user.uid));
|
|
|
|
|
currentStepsData = stepsSnap.exists() ? stepsSnap.data().stepsByDate || {} : {};
|
|
|
|
|
loadDashboard();
|
|
|
|
|
const pendingCount = getPendingFriendUids().length;
|
|
|
|
|
if (pendingCount > 0) switchPanel('friends');
|
|
|
|
|
if (!currentUserData.setupComplete && currentUserData.approved === false) {
|
|
|
|
|
await updateDoc(userRef, { approved: true });
|
|
|
|
|
currentUserData.approved = true;
|
|
|
|
|
}
|
|
|
|
|
const profileSnap = await getDoc(doc(db, 'profiles', user.uid));
|
|
|
|
|
currentProfileData = profileSnap.exists() ? profileSnap.data() : {};
|
|
|
|
|
const stepsSnap = await getDoc(doc(db, 'steps', user.uid));
|
|
|
|
|
currentStepsData = stepsSnap.exists() ? stepsSnap.data().stepsByDate || {} : {};
|
|
|
|
|
loadDashboard();
|
|
|
|
|
const pendingCount = getPendingFriendUids().length;
|
|
|
|
|
if (pendingCount > 0) switchPanel('friends');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function notifyAdminOfNewUser(email, uid) {
|
|
|
|
|
try {
|
|
|
|
|
await fetch(`${window.STEPMATES_CONFIG.adminNotifyUrl}?uid=${uid}&email=${encodeURIComponent(email)}`);
|
|
|
|
|
} catch(e) { console.warn('Admin notify failed', e); }
|
|
|
|
|
// Admin approval removed - auto-approval enabled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ONBOARDING
|
|
|
|
@@ -857,8 +889,9 @@ function refreshProfileUI() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setAvatarEl(wrapEl, initialId, initial, url, size) {
|
|
|
|
|
if (url) wrapEl.innerHTML = `<img src="${esc(url)}" alt="${initial}" onerror="this.parentElement.innerHTML='${initial}'">`;
|
|
|
|
|
else wrapEl.innerHTML = `<span id="${initialId}">${initial}</span>`;
|
|
|
|
|
if (url && isEmoji(url)) wrapEl.innerHTML = `<span style="font-size:${Math.round(size*0.55)}px">${url}</span>`;
|
|
|
|
|
else if (url && url.startsWith('http')) wrapEl.innerHTML = `<img src="${esc(url)}" alt="${initial}" onerror="this.parentElement.innerHTML='🏃'">`;
|
|
|
|
|
else wrapEl.innerHTML = `<span style="font-size:${Math.round(size*0.55)}px">🏃</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function checkSetupReminder() {
|
|
|
|
@@ -916,10 +949,10 @@ function loadFriendsRealtime() {
|
|
|
|
|
friendSteps = {};
|
|
|
|
|
snap.forEach(d => { friendsData[d.id] = d.data(); });
|
|
|
|
|
const approvedUids = Object.entries(friendsData)
|
|
|
|
|
.filter(([uid, data]) => data.approved === true && data.enabled === true)
|
|
|
|
|
.filter(([uid, data]) => data.status === 'active')
|
|
|
|
|
.map(([uid]) => uid);
|
|
|
|
|
const pendingUids = Object.entries(friendsData)
|
|
|
|
|
.filter(([uid, data]) => data.approved === false && data.enabled !== false)
|
|
|
|
|
.filter(([uid, data]) => data.status === 'pending' && data.initiator !== 'me')
|
|
|
|
|
.map(([uid]) => uid);
|
|
|
|
|
await loadFriendProfilesAndSteps(approvedUids);
|
|
|
|
|
for (const uid of pendingUids) {
|
|
|
|
@@ -950,13 +983,13 @@ function loadFriendsRealtime() {
|
|
|
|
|
|
|
|
|
|
function getMutualFriendUids() {
|
|
|
|
|
return Object.entries(friendsData)
|
|
|
|
|
.filter(([uid, data]) => data.approved === true && data.enabled === true)
|
|
|
|
|
.filter(([uid, data]) => data.status === 'active')
|
|
|
|
|
.map(([uid]) => uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPendingFriendUids() {
|
|
|
|
|
return Object.entries(friendsData)
|
|
|
|
|
.filter(([uid, data]) => data.approved === false && data.enabled !== false)
|
|
|
|
|
.filter(([uid, data]) => data.status === 'pending' && data.initiator !== 'me')
|
|
|
|
|
.map(([uid]) => uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1136,7 +1169,7 @@ window.bulkRemoveFriends = async function() {
|
|
|
|
|
async function doRemoveFriend(friendUid) {
|
|
|
|
|
const idToken = await currentUser.getIdToken();
|
|
|
|
|
await fetch(`${window.STEPMATES_CONFIG.friendRequestUrl}?action=remove&fromUid=${currentUser.uid}&toUid=${friendUid}&idToken=${idToken}`);
|
|
|
|
|
if (friendsData[friendUid]) friendsData[friendUid].enabled = false;
|
|
|
|
|
if (friendsData[friendUid]) friendsData[friendUid].status = 'removed';
|
|
|
|
|
delete friendProfiles[friendUid];
|
|
|
|
|
delete friendSteps[friendUid];
|
|
|
|
|
renderLeaderboard();
|
|
|
|
@@ -1144,13 +1177,19 @@ async function doRemoveFriend(friendUid) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.approveFriend = async function(uid) {
|
|
|
|
|
await updateDoc(doc(db, 'friends', currentUser.uid, 'list', uid), { approved: true });
|
|
|
|
|
await loadFriendProfilesAndSteps([uid]);
|
|
|
|
|
showToast('Friend added! 🎉', 'success');
|
|
|
|
|
try {
|
|
|
|
|
const idToken = await currentUser.getIdToken();
|
|
|
|
|
await fetch(`${STEPMATES_CONFIG.acceptFriendUrl}?fromUid=${currentUser.uid}&requesterUid=${uid}&idToken=${encodeURIComponent(idToken)}`);
|
|
|
|
|
await loadFriendProfilesAndSteps([uid]);
|
|
|
|
|
showToast('Friend added! 🎉', 'success');
|
|
|
|
|
} catch(e) {
|
|
|
|
|
await updateDoc(doc(db, 'friends', currentUser.uid, 'list', uid), { status: 'active' });
|
|
|
|
|
showToast('Friend added! 🎉', 'success');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.denyFriend = async function(uid) {
|
|
|
|
|
await updateDoc(doc(db, 'friends', currentUser.uid, 'list', uid), { enabled: false });
|
|
|
|
|
await updateDoc(doc(db, 'friends', currentUser.uid, 'list', uid), { status: 'removed' });
|
|
|
|
|
showToast('Request declined', '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1163,14 +1202,29 @@ window.openFriendProfile = async function(uid) {
|
|
|
|
|
const displayName = profile.displayName || 'No Display Name';
|
|
|
|
|
|
|
|
|
|
const avatarEl = document.getElementById('fp-avatar');
|
|
|
|
|
if (profile.avatarUrl) avatarEl.innerHTML = `<img src="${esc(profile.avatarUrl)}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
|
|
|
|
|
else avatarEl.textContent = (nick || displayName)[0].toUpperCase();
|
|
|
|
|
if (profile.avatarUrl && isEmoji(profile.avatarUrl)) {
|
|
|
|
|
avatarEl.innerHTML = `<span style="font-size:32px">${profile.avatarUrl}</span>`;
|
|
|
|
|
} else if (profile.avatarUrl) {
|
|
|
|
|
avatarEl.innerHTML = `<img src="${esc(profile.avatarUrl)}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
|
|
|
|
|
} else {
|
|
|
|
|
avatarEl.textContent = (nick || displayName)[0].toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('fp-name-text').textContent = nick || displayName;
|
|
|
|
|
|
|
|
|
|
const addedAt = friendsData[uid]?.addedAt?.toDate?.();
|
|
|
|
|
document.getElementById('fp-joined').textContent = addedAt ? addedAt.toLocaleDateString('en-US', {month:'long', day:'numeric', year:'numeric'}) : '—';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const stepsDoc = await getDoc(doc(db, 'steps', uid));
|
|
|
|
|
const syncTs = stepsDoc.data()?.lastSynced?.toDate?.();
|
|
|
|
|
document.getElementById('fp-last-synced').textContent = syncTs
|
|
|
|
|
? syncTs.toLocaleString('en-US', {month:'short', day:'numeric', year:'numeric', hour:'numeric', minute:'2-digit'})
|
|
|
|
|
: 'Never';
|
|
|
|
|
} catch(e) {
|
|
|
|
|
document.getElementById('fp-last-synced').textContent = 'Never';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showScreen('friend-profile');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1280,6 +1334,25 @@ window.sendMagicLink = async function() {
|
|
|
|
|
btn.disabled = false; btn.innerHTML = 'Continue with Email';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.signInWithGoogle = async function() {
|
|
|
|
|
const btn = document.getElementById('google-signin-btn');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.innerHTML = '<span class="spinner" style="color:#4285F4"></span> Signing in…';
|
|
|
|
|
try {
|
|
|
|
|
const provider = new GoogleAuthProvider();
|
|
|
|
|
const result = await signInWithPopup(auth, provider);
|
|
|
|
|
_initializingUser = true;
|
|
|
|
|
await initUser(result.user);
|
|
|
|
|
} catch(e) {
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 18 18" style="flex-shrink:0"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.184l-2.908-2.258c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z"/><path fill="#FBBC05" d="M3.964 10.707c-.18-.54-.282-1.117-.282-1.707s.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.55 0 9s.348 2.825.957 4.039l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.961L3.964 7.293C4.672 5.166 6.656 3.58 9 3.58z"/></svg> Sign in with Google`;
|
|
|
|
|
if (e.code !== 'auth/popup-closed-by-user' && e.code !== 'auth/cancelled-popup-request') {
|
|
|
|
|
document.getElementById('auth-error').textContent = 'Google sign-in failed. Please try again or use email.';
|
|
|
|
|
document.getElementById('auth-error').style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.showAuthForm = function() {
|
|
|
|
|
document.getElementById('auth-card').style.display = 'block';
|
|
|
|
|
document.getElementById('magic-sent-card').style.display = 'none';
|
|
|
|
@@ -1290,7 +1363,8 @@ window.showAuthForm = function() {
|
|
|
|
|
// PROFILE MODAL
|
|
|
|
|
window.openProfileModal = function() {
|
|
|
|
|
document.getElementById('profile-name-input').value = currentProfileData?.displayName || '';
|
|
|
|
|
document.getElementById('profile-avatar-input').value = currentProfileData?.avatarUrl || '';
|
|
|
|
|
const avatarUrl = currentProfileData?.avatarUrl || '';
|
|
|
|
|
document.getElementById('profile-avatar-input').value = isEmoji(avatarUrl) ? avatarUrl : '';
|
|
|
|
|
document.getElementById('profile-error').style.display = 'none';
|
|
|
|
|
updateProfilePreview();
|
|
|
|
|
document.getElementById('profile-modal').classList.add('open');
|
|
|
|
@@ -1298,14 +1372,22 @@ window.openProfileModal = function() {
|
|
|
|
|
}
|
|
|
|
|
window.closeProfileModal = function() { document.getElementById('profile-modal').classList.remove('open'); }
|
|
|
|
|
window.closeProfileModalOutside = function(e) { if (e.target===document.getElementById('profile-modal')) closeProfileModal(); }
|
|
|
|
|
function updateEmojiPreview(val) {
|
|
|
|
|
const preview = document.getElementById('emoji-preview');
|
|
|
|
|
if (!preview) return;
|
|
|
|
|
const emoji = val.trim();
|
|
|
|
|
preview.textContent = emoji || '🏃';
|
|
|
|
|
}
|
|
|
|
|
function updateProfilePreview() {
|
|
|
|
|
const name = document.getElementById('profile-name-input').value.trim() || 'Your Name';
|
|
|
|
|
const url = document.getElementById('profile-avatar-input').value.trim();
|
|
|
|
|
const initial = name[0].toUpperCase();
|
|
|
|
|
document.getElementById('profile-modal-name-preview').textContent = name;
|
|
|
|
|
const avatarEl = document.getElementById('profile-modal-avatar');
|
|
|
|
|
if (url) avatarEl.innerHTML = `<img src="${esc(url)}" alt="${initial}" onerror="this.parentElement.innerHTML='${initial}'">`;
|
|
|
|
|
if (url && isEmoji(url)) avatarEl.innerHTML = `<span style="font-size:28px">${url}</span>`;
|
|
|
|
|
else if (url) avatarEl.innerHTML = `<img src="${esc(url)}" alt="${initial}" onerror="this.parentElement.innerHTML='${initial}'">`;
|
|
|
|
|
else avatarEl.innerHTML = `<span id="profile-modal-initial">${initial}</span>`;
|
|
|
|
|
updateEmojiPreview(url);
|
|
|
|
|
}
|
|
|
|
|
window.saveProfile = async function() {
|
|
|
|
|
const name = document.getElementById('profile-name-input').value.trim();
|
|
|
|
@@ -1313,7 +1395,7 @@ window.saveProfile = async function() {
|
|
|
|
|
const errEl = document.getElementById('profile-error');
|
|
|
|
|
errEl.style.display = 'none';
|
|
|
|
|
if (!name || name.length < 2) { errEl.textContent = 'Display name must be at least 2 characters.'; errEl.style.display = 'block'; return; }
|
|
|
|
|
if (avatarUrl && !avatarUrl.startsWith('http')) { errEl.textContent = 'Avatar URL must start with http:// or https://'; errEl.style.display = 'block'; return; }
|
|
|
|
|
if (avatarUrl && !avatarUrl.startsWith('http') && !isEmoji(avatarUrl)) { errEl.textContent = 'Please enter a valid emoji'; errEl.style.display = 'block'; return; }
|
|
|
|
|
const btn = document.querySelector('#profile-modal .btn-primary');
|
|
|
|
|
btn.disabled = true; btn.innerHTML = '<span class="spinner"></span>';
|
|
|
|
|
try {
|
|
|
|
@@ -1386,8 +1468,11 @@ window.signOut = async function() {
|
|
|
|
|
if (!confirm('Sign out?')) return;
|
|
|
|
|
teardown();
|
|
|
|
|
await fbSignOut(auth);
|
|
|
|
|
_initializingUser = false;
|
|
|
|
|
currentUser = null; currentUserData = null; currentProfileData = null; currentStepsData = null;
|
|
|
|
|
friendsData = {}; friendProfiles = {}; friendSteps = {};
|
|
|
|
|
const btn = document.getElementById('google-signin-btn');
|
|
|
|
|
if (btn) { btn.disabled = false; btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 18 18" style="flex-shrink:0"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.184l-2.908-2.258c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z"/><path fill="#FBBC05" d="M3.964 10.707c-.18-.54-.282-1.117-.282-1.707s.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.55 0 9s.348 2.825.957 4.039l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.961L3.964 7.293C4.672 5.166 6.656 3.58 9 3.58z"/></svg> Sign in with Google`; }
|
|
|
|
|
showScreen('auth'); showAuthForm();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|