// screens-flows.jsx — sheets and flows used across the app:
// • ClassSheet (slide-up class detail; "Book" → review → confirmed)
// • WaitlistSheet (when class is full)
// • PackagesSheet (grid of packs to buy) + CheckoutSheet
// • SplashScreen / SignInScreen (returning member focused)
// • SettingsSheet (notification prefs etc)
const { fmtIDR, findClass, findCoach, findFriend, minutesUntil, fmtCountdown, DAYS_SHORT, DAYS_LONG, DATES_THIS_WEEK } = window.NADI_HELPERS;
// ── Class detail sheet (slide-up; first step of booking) ─────────────
function ClassSheet({ session, user, onClose, onBookConfirmed, onWaitlist, onBuyPack, toggleSavedClass, toggleSavedCoach, openFriendProfile }) {
const [reviewing, setReviewing] = useState(false);
const [attendAnon, setAttendAnon] = useState(false);
// Reset privacy choice each time a fresh session opens; default to user pref
useEffect(() => {
if (session) {
setReviewing(false);
setAttendAnon(!user.visibleByDefault);
}
}, [session && session.id]);
if (!session) return null;
const c = findClass(session.classId);
const co = findCoach(session.coachId);
const left = session.capacity - session.booked;
const full = left === 0;
const booked = user.booked.includes(session.id);
const onWait = user.waitlist.includes(session.id);
const friends = (session.friends || []).map(findFriend).filter(Boolean);
const isFav = user.savedClasses.includes(c.id);
// packs sorted by expiry — used to show which credit funds this
const packs = (user.creditPacks || []).slice().sort((a, b) => new Date(a.expires) - new Date(b.expires));
const fundingPack = packs.find((p) => p.credits > 0);
const fmtDate = (iso) => new Date(iso).toLocaleDateString("en-GB", { day: "2-digit", month: "short" }).toLowerCase();
// Two screens slide horizontally inside the sheet body
return (
{/* ── DETAIL ── */}
{DAYS_LONG[session.day].toLowerCase()} · {session.time} · {c.duration} min
close ×
{c.name}
{c.tagline}.
{/* Teacher block */}
toggleSavedCoach && toggleSavedCoach(co.id)} className="press" style={{
marginTop: 22, width: "100%",
padding: "14px 16px",
background: "var(--surface)", border: "1px solid var(--rule)",
display: "flex", alignItems: "center", gap: 14,
textAlign: "left",
}}>
{co.name}
{co.title.toLowerCase()} · {co.yrs} yrs
{/* Detail grid */}
{/* Friends going */}
{friends.length > 0 && (
going
{friends.length} from your circle
{session.anonCount > 0 ? ` · ${session.anonCount} anon` : ""}
{friends.slice(0, 6).map((f) => (
openFriendProfile && openFriendProfile(f)} className="press" style={{
padding: "5px 10px 5px 5px",
border: "1px solid var(--rule)", background: "var(--bg)",
display: "inline-flex", alignItems: "center", gap: 8,
borderRadius: 999,
}}>
{f.firstName || f.name}
))}
)}
{/* About this class */}
about this class
A {c.duration}-minute class for {c.level.toLowerCase()} practitioners. Slow warm-up, deliberate sequencing, and time at the end for stillness. Mats and props provided.
{/* Save toggle */}
toggleSavedClass && toggleSavedClass(c.id)} className="press" style={{
marginTop: 20, width: "100%",
display: "flex", alignItems: "center", justifyContent: "center", gap: 10,
padding: "12px 14px",
border: `1px solid ${isFav ? "var(--primary)" : "var(--rule)"}`,
background: "transparent",
color: isFav ? "var(--primary)" : "var(--ink-soft)",
}}>
{isFav ? "saved · tap to remove" : "save this class"}
{/* Sticky CTA */}
{booked ? (
Already booked
) : full ? (
onWait ? (
On waitlist · we'll notify you
) : (
onWaitlist(session)}>Join waitlist
)
) : fundingPack ? (
setReviewing(true)}>
Continue · 1 credit
) : (
Buy a pack to book →
)}
cancel up to 1h before · credit refunded
{/* ── REVIEW (step 2) ── */}
setReviewing(false)} className="press" style={{
background: "transparent", display: "inline-flex", alignItems: "center", gap: 6,
}}>
back
booking · review
Confirm your seat.
1 credit will be used.
{DAYS_LONG[session.day].toLowerCase()} · {session.time}
{c.name}
with {co.name.toLowerCase()} · {session.roomShort.toLowerCase()}
{fundingPack && (
credit from
{fundingPack.packageName}
expires {fmtDate(fundingPack.expires)}
{fundingPack.credits}
{fundingPack.credits - 1}
credit{fundingPack.credits - 1 === 1 ? "" : "s"} left
)}
{/* Add to calendar toggle */}
Add to calendar
plus a 30-min before reminder
{/* Anonymous attendance toggle */}
{attendAnon ? "Going anonymously" : "Going as Sinta"}
{attendAnon ? "hidden from your friends" : "visible to your circle"}
{ onBookConfirmed(session, attendAnon ? "anon" : "self"); }}>
Confirm booking
you can cancel up to 1h before class
);
}
function DetailCell({ label, value, sub, accent }) {
return (
);
}
function ToggleSwitch({ defaultOn, onChange }) {
const [on, setOn] = useState(!!defaultOn);
const flip = () => {
const next = !on;
setOn(next);
onChange && onChange(next);
};
return (
);
}
// ── Confirmation full-screen ─────────────────────────────────────────
function BookingConfirmed({ open, session, onClose }) {
if (!open || !session) return null;
const c = findClass(session.classId), co = findCoach(session.coachId);
return (
You're on the mat.
{c.name} · {DAYS_LONG[session.day]} at {session.time}. with {co.name}.
we'll send a quiet reminder.
Done
);
}
// ── Waitlist sheet ───────────────────────────────────────────────────
function WaitlistSheet({ open, session, onClose, onConfirm }) {
if (!open || !session) return null;
const c = findClass(session.classId), co = findCoach(session.coachId);
return (
waitlist · this class is full
close ×
Want a seat if one opens?
{DAYS_LONG[session.day].toLowerCase()} · {session.time}
{c.name}
with {co.name.toLowerCase()}
{[
"We'll text you the moment a seat opens.",
"You'll have 10 minutes to grab it.",
"No credit is used unless you book."
].map((t, i) => (
{t}
))}
Not now
{ onConfirm(session); onClose(); }}>Join waitlist
);
}
// ── Packages sheet ──────────────────────────────────────────────────
function PackagesSheet({ open, onClose, onChoose }) {
return (
packages · every pack 30-day
close ×
Practice often , pay less.
Buy a pack of class credits. Every pack is good for thirty days. Use them on any class, any teacher.
{window.NADI.PACKAGES.map((p) => {
const isStd = p.id === "five";
return (
onChoose(p)} className="press" style={{
width: "100%", textAlign: "left",
padding: "18px 20px",
background: isStd ? "var(--ink)" : "var(--bg)",
color: isStd ? "var(--bg)" : "var(--ink)",
border: isStd ? "1px solid var(--ink)" : "1px solid var(--rule)",
display: "grid", gridTemplateColumns: "1fr auto", gap: 12, alignItems: "center",
}}>
{p.badge &&
{p.badge.toLowerCase()} }
{p.name}
{p.classes} class{p.classes === 1 ? "" : "es"} · {fmtIDR(Math.round(p.price / p.classes))}/class
{fmtIDR(p.price)}
choose →
);
})}
how it works
{[
["01", "Buy a pack — valid 30 days."],
["02", "Spend one credit per class, any teacher."],
["03", "Cancel up to 1h before to get the credit back."],
].map(([n, t]) => (
{n}
{t}
))}
);
}
// ── Checkout sheet (payment method) ─────────────────────────────────
// Bank transfer is the only enabled method for now — QRIS and Card are
// shown as "coming soon" so members still see the roadmap.
function CheckoutSheet({ pkg, onClose, onConfirm }) {
const method = "Bank Transfer";
if (!pkg) return null;
const methods = [
{ id: "Bank Transfer", sub: "transfer & upload proof", enabled: true },
{ id: "QRIS", sub: "coming soon", enabled: false },
{ id: "Card", sub: "coming soon", enabled: false },
];
return (
checkout
close ×
{pkg.name}.
{pkg.classes} class{pkg.classes !== 1 ? "es" : ""} · valid {pkg.validityDays} days
Total
{fmtIDR(pkg.price)}
payment
{methods.map((m) => {
const sel = m.id === method;
return (
{m.enabled ? (
) : (
soon
)}
);
})}
onConfirm(pkg, method)}>Confirm purchase
next: transfer & upload your proof
);
}
// ── Bank transfer sheet (after checkout: account list + upload proof) ────
function BankTransferSheet({ pkg, onClose, onSubmit }) {
const [selected, setSelected] = useState(null);
const [copied, setCopied] = useState(null);
const [proof, setProof] = useState(null); // {name, dataUrl}
const [note, setNote] = useState("");
const fileRef = React.useRef(null);
// pick first active bank by default when sheet opens
useEffect(() => {
if (pkg) {
const banks = (window.NADI_ADMIN.BANK_ACCOUNTS || []).filter((b) => b.active);
setSelected(banks[0]?.id || null);
setProof(null); setNote(""); setCopied(null);
}
}, [pkg && pkg.id]);
if (!pkg) return null;
const banks = (window.NADI_ADMIN.BANK_ACCOUNTS || []).filter((b) => b.active);
const copy = (text, key) => {
try { navigator.clipboard && navigator.clipboard.writeText(text); } catch (e) {}
setCopied(key);
setTimeout(() => setCopied(null), 1400);
};
const onPick = (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = () => setProof({ name: f.name, dataUrl: reader.result, size: f.size });
reader.readAsDataURL(f);
};
const canSubmit = !!selected && !!proof;
const submit = () => {
if (!canSubmit) return;
onSubmit && onSubmit({ pkg, bankId: selected, proof, note });
};
return (
step 2 of 2 · bank transfer
close ×
Transfer {fmtIDR(pkg.price)}
Send to any of the accounts below, then upload a screenshot of your receipt. The studio will confirm within a few hours and your credits will be activated.
{/* Order summary card */}
{pkg.name}
{pkg.classes} credit{pkg.classes === 1 ? "" : "s"} · valid {pkg.validityDays}d
{fmtIDR(pkg.price)}
{/* Bank accounts */}
01 · transfer to one of
{banks.length === 0 && (
no accounts configured · contact studio
)}
{banks.map((b) => {
const sel = selected === b.id;
return (
setSelected(b.id)} className="press" style={{
width: "100%", textAlign: "left", background: "transparent",
display: "grid", gridTemplateColumns: "20px 1fr", gap: 12, alignItems: "center",
}}>
{sel && }
{b.bank}
{b.branch.toLowerCase()}
{b.number}
copy(b.number.replace(/\s/g, ""), b.id + "-num")} className="press" style={{
padding: "4px 8px", border: "1px solid var(--rule)", background: "var(--surface)",
fontFamily: "'JetBrains Mono', monospace", fontSize: 10,
letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink)",
}}>{copied === b.id + "-num" ? "copied ✓" : "copy"}
a/n {b.name.toLowerCase()}
);
})}
copy(String(pkg.price), "amount")} className="press" style={{
marginTop: 12, width: "100%", padding: "12px 14px",
border: "1px dashed var(--rule-strong)", background: "transparent",
display: "flex", justifyContent: "space-between", alignItems: "center",
}}>
exact amount · {fmtIDR(pkg.price)}
{copied === "amount" ? "copied ✓" : "tap to copy"}
{/* Upload proof */}
02 · upload your receipt
{!proof ? (
fileRef.current && fileRef.current.click()} className="press" style={{
marginTop: 12, width: "100%", padding: "26px 14px",
border: "1px dashed var(--rule-strong)", background: "var(--surface)",
display: "flex", flexDirection: "column", alignItems: "center", gap: 8,
}}>
Choose screenshot
jpg or png · the transfer confirmation
) : (
fileRef.current && fileRef.current.click()} className="press" style={{
padding: "6px 10px", border: "1px solid var(--rule)", background: "var(--surface)",
fontFamily: "'JetBrains Mono', monospace", fontSize: 10,
letterSpacing: "0.08em", textTransform: "uppercase",
}}>replace
)}
{/* Optional note */}
note (optional)
setNote(e.target.value)}
placeholder="e.g. transferred from my BCA account"
style={{
marginTop: 10, width: "100%", padding: "12px 14px",
border: "1px solid var(--rule)", background: "var(--bg)",
fontFamily: "Newsreader, serif", fontSize: 15, color: "var(--ink)",
}} />
{canSubmit ? "Submit for confirmation" : (proof ? "Pick a bank account" : "Upload your receipt")}
credits activate once the studio verifies your transfer
);
}
// ── Purchase pending confirmation (after submitting proof) ──────────
function PurchaseSubmittedSheet({ open, pkg, onClose }) {
if (!pkg) return null;
return (
received · awaiting confirmation
Thank you,we'll verify shortly.
Your {pkg.name} is pending. Once the studio matches your transfer we'll activate your {pkg.classes} credit{pkg.classes === 1 ? "" : "s"} and notify you.
Done
);
}
// ── Splash / Sign-in (returning member focused) ─────────────────────
function SplashScreen({ onSignIn, onContinue, onAuth }) {
const { useState, useEffect, useRef } = React;
const googleBtnRef = useRef(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const finishGoogle = async (credential) => {
setLoading(true);
try {
const res = await fetch("/api/auth/google", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ credential }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "Google authentication failed");
}
const data = await res.json();
localStorage.setItem("nadi_token", data.access_token);
if (onAuth) {
onAuth(data.user);
} else {
window.location.reload();
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
let cancelled = false;
let attempts = 0;
const renderGoogleButton = () => {
fetch("/api/config")
.then((r) => r.ok ? r.json() : { googleClientId: "" })
.then((d) => {
const clientId = d.googleClientId;
if (!clientId || cancelled) {
renderMockButton();
return;
}
const checkGis = () => {
if (window.google && window.google.accounts && window.google.accounts.id) {
window.google.accounts.id.initialize({
client_id: clientId,
callback: (resp) => { if (resp && resp.credential) finishGoogle(resp.credential); },
ux_mode: "popup",
auto_select: false,
});
if (googleBtnRef.current) {
googleBtnRef.current.innerHTML = "";
window.google.accounts.id.renderButton(googleBtnRef.current, {
type: "standard",
theme: "outline",
size: "large",
text: "continue_with",
shape: "rectangular",
logo_alignment: "center",
width: 320,
});
}
} else if (attempts++ < 50) {
setTimeout(checkGis, 100);
}
};
checkGis();
})
.catch(() => {
if (!cancelled) renderMockButton();
});
};
const renderMockButton = () => {
if (googleBtnRef.current) {
googleBtnRef.current.innerHTML = "";
const btn = document.createElement("button");
btn.className = "press";
btn.style.width = "100%";
btn.style.padding = "14px 18px";
btn.style.background = "var(--bg)";
btn.style.border = "1px solid var(--rule)";
btn.style.display = "flex";
btn.style.alignItems = "center";
btn.style.justifyContent = "center";
btn.style.gap = "12px";
btn.innerHTML = `
Continue with Google
`;
btn.onclick = () => {
if (onAuth) {
onAuth({ name: "Sinta Putri", email: "sinta@mail.com" });
} else if (onContinue) {
onContinue();
}
};
googleBtnRef.current.appendChild(btn);
}
};
renderGoogleButton();
return () => { cancelled = true; };
}, [onAuth, onContinue]);
return (
{/* Ambient breath */}
est. 2026
suvarna sutera · tangerang
Breath aspractice.
The body ashome.
A small studio for the slow, attentive practice — in two open-air rooms above the lake.
{error && (
{error}
)}
{/* Google sign-in button container */}
Sign in or sign up with email
tangerang · open daily 06:00 — 21:00
);
}
// ── Sign-in screen (full-screen, mobile-native) ─────────────────────
// ── Sign-in screen (full-screen, mobile-native) ─────────────────────
function SignInScreen({ onClose, onAuth }) {
const [mode, setMode] = useState("signin"); // signin | register
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const googleBtnRef = React.useRef(null);
const API_BASE = window.location.origin.includes("localhost") ? "http://localhost:8001" : "";
// Google Sign-In finish
const finishGoogle = async (credential) => {
setError(null);
setLoading(true);
try {
const res = await fetch(API_BASE + "/api/auth/google", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ credential }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "Google authentication failed");
}
const data = await res.json();
localStorage.setItem("nadi_token", data.access_token);
onAuth(data.user);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Load Google client ID
useEffect(() => {
let cancelled = false;
let attempts = 0;
const renderGoogleButton = () => {
fetch(API_BASE + "/api/config")
.then((r) => r.ok ? r.json() : { googleClientId: "" })
.then((d) => {
const clientId = d.googleClientId;
if (!clientId || cancelled) return;
const checkGis = () => {
if (window.google && window.google.accounts && window.google.accounts.id) {
window.google.accounts.id.initialize({
client_id: clientId,
callback: (resp) => { if (resp && resp.credential) finishGoogle(resp.credential); },
ux_mode: "popup",
auto_select: false,
});
if (googleBtnRef.current) {
googleBtnRef.current.innerHTML = "";
window.google.accounts.id.renderButton(googleBtnRef.current, {
type: "standard",
theme: "outline",
size: "large",
text: mode === "signin" ? "signin_with" : "signup_with",
shape: "rectangular",
logo_alignment: "center",
width: 320,
});
}
} else if (attempts++ < 50) {
setTimeout(checkGis, 100);
}
};
checkGis();
})
.catch(() => {});
};
renderGoogleButton();
return () => { cancelled = true; };
}, [mode]);
const submit = async () => {
setError(null);
setLoading(true);
try {
const endpoint = mode === "signin" ? "/api/auth/signin" : "/api/auth/register";
const body = mode === "signin"
? { email, password }
: { name, email, password };
const res = await fetch(API_BASE + endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "Authentication failed");
}
const data = await res.json();
localStorage.setItem("nadi_token", data.access_token);
onAuth(data.user);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
{mode === "signin" ? <>Welcome back. > : <>Make a start. >}
{mode === "signin" ? "Sign in to book classes and check your credits." : "Tell us where to send your booking confirmations."}
{mode === "register" && }
{error && (
{error}
)}
{loading ? "Please wait..." : (mode === "signin" ? "Sign in" : "Create account")}
setMode(mode === "signin" ? "register" : "signin")} className="press" style={{ background: "transparent", padding: 6 }}>
{mode === "signin" ? "new here? create an account →" : "already a member? sign in →"}
);
}
function GoogleG({ size = 18 }) {
return (
);
}
// ── Settings sheet ─────────────────────────────────────────────────
function SettingsSheet({ open, onClose, onSignOut, user, setUser, showToast }) {
return (
settings
close ×
Preferences.
{/* Privacy */}
privacy
Show me to friends
your default — you can switch per class
{
if (setUser && user) setUser({ ...user, visibleByDefault: v });
showToast && showToast(v ? "Visible to your friends." : "Going incognito by default.");
}} />
{user &&
}
{user && user.visibleByDefault
? "friends will see you in class lists"
: "you'll appear as +1 anon in their feed"}
notifications
{[
{ label: "Class reminders", sub: "30 min before · push", on: true },
{ label: "Waitlist alerts", sub: "the moment a seat opens", on: true },
{ label: "Pack expiry warnings", sub: "7 days before credit lapse", on: true },
{ label: "Friend activity", sub: "when someone you follow books a shared class", on: true },
{ label: "Weekly schedule digest", sub: "every monday morning", on: false },
{ label: "New teacher / class news", sub: "rare and slow", on: false },
].map((item, i) => (
))}
Sign out
);
}
Object.assign(window, {
ClassSheet, BookingConfirmed, WaitlistSheet, PackagesSheet, CheckoutSheet,
BankTransferSheet, PurchaseSubmittedSheet,
SplashScreen, SignInScreen, SettingsSheet,
FindFriendsSheet, FriendProfileSheet,
ToggleSwitch,
});
// ── Find Friends sheet ─────────────────────────────────────────────
function FindFriendsSheet({ open, onClose, user, sendRequest, openFriendProfile, showToast }) {
const [query, setQuery] = useState("");
const suggestions = window.NADI.FRIEND_SUGGESTIONS;
const filtered = query
? suggestions.filter((s) => s.name.toLowerCase().includes(query.toLowerCase()))
: suggestions;
const sent = (id) => user.sentRequests.includes(id);
return (
friends · find people
close ×
Find your circle .
Practice is quieter when someone you know is on the next mat. Follow people you've shared a class with.
{/* Search */}
{/* Quick add methods */}
showToast && showToast("QR scanner opened.")} className="press" style={{
padding: "14px 12px",
background: "var(--bg)", border: "1px solid var(--rule)",
display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 6,
}}>
Scan QR
at the front desk
{ navigator.clipboard?.writeText("nadi.yoga/u/sinta"); showToast && showToast("Invite link copied."); }} className="press" style={{
padding: "14px 12px",
background: "var(--bg)", border: "1px solid var(--rule)",
display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 6,
}}>
Copy invite link
send via any app
{/* Suggestions */}
suggested · {filtered.length}
people you've shared classes with
{filtered.length === 0 ? (
no matches
) : filtered.map((s) => (
openFriendProfile && openFriendProfile(s)} className="press" style={{ background: "transparent", padding: 0 }}>
openFriendProfile && openFriendProfile(s)} className="press" style={{ background: "transparent", textAlign: "left", minWidth: 0 }}>
{s.name}
{s.reason} · {s.mutual} mutual
sendRequest(s.id)} disabled={sent(s.id)} className="press" style={{
padding: "8px 14px",
background: sent(s.id) ? "transparent" : "var(--ink)",
color: sent(s.id) ? "var(--ink-soft)" : "var(--bg)",
border: `1px solid ${sent(s.id) ? "var(--rule)" : "var(--ink)"}`,
fontFamily: "'JetBrains Mono', monospace", fontSize: 10,
letterSpacing: "0.06em", textTransform: "lowercase",
}}>{sent(s.id) ? "requested" : "+ follow"}
))}
);
}
// ── Friend profile sheet ───────────────────────────────────────────
function FriendProfileSheet({ friend, user, onClose, unfollowFriend, sendRequest, acceptRequest, declineRequest, showToast }) {
if (!friend) return null;
const isFollowing = user.following.includes(friend.id);
const isPending = !!friend.isPending;
const requested = user.sentRequests.includes(friend.id);
// Find shared classes — sessions where this friend goes that current user has saved/booked
const sharedSessions = window.NADI.SCHEDULE
.filter((s) => s.friends.includes(friend.id))
.slice(0, 5);
const nextShared = sharedSessions
.filter((s) => user.booked.includes(s.id))[0];
// Mock stats
const stats = {
classesThisMonth: 6,
favouriteClass: "Slow Flow",
favouriteTeacher: "Ayu",
practicing: "since Mar 2025",
};
return (
{isPending ? "follow request" : isFollowing ? "in your circle" : "discover"}
close ×
{friend.name}
practicing {stats.practicing.toLowerCase()}
{/* Mutuals + meta */}
{(friend.mutual || friend.reason) && (
{friend.reason || `${friend.mutual || 0} mutual friend${(friend.mutual || 0) === 1 ? "" : "s"}`}
)}
{/* Stats */}
this month
{stats.classesThisMonth}
classes
often books
{stats.favouriteClass}
with {stats.favouriteTeacher.toLowerCase()}
{/* Shared upcoming */}
{nextShared && (
you're both going
{window.NADI_HELPERS.findClass(nextShared.classId).name}
{window.NADI_HELPERS.DAYS_LONG[nextShared.day].toLowerCase()} · {nextShared.time}
)}
{/* Recent shared classes */}
{sharedSessions.length > 0 && (
recent shared classes
{sharedSessions.map((s) => {
const c = window.NADI_HELPERS.findClass(s.classId);
return (
{c.name}
{window.NADI_HELPERS.DAYS_SHORT[s.day].toLowerCase()} {s.time}
);
})}
)}
{/* CTA */}
{isPending ? (
{ declineRequest && declineRequest(friend.id); onClose(); }}>Decline
{ acceptRequest && acceptRequest(friend.id); onClose(); }}>Follow back
) : isFollowing ? (
{ unfollowFriend && unfollowFriend(friend.id); onClose(); }}>Unfollow
) : requested ? (
Request sent
) : (
{ sendRequest && sendRequest(friend.id); }}>+ Follow
)}
);
}