Dating — Edit Profile
A playful mobile-app edit-profile screen wrapped in a phone frame: a six-slot photo grid you drag to reorder, a bio textarea with a live character counter, toggleable interest chips, tappable prompt cards you pick and answer, and a live profile-completeness meter that climbs as you fill things in. Gradient coral-to-violet accents, pill buttons, rounded surfaces, and smooth toast feedback make it feel like a real dating app.
MCP
Code
:root {
--bg: #fff5f6;
--surface: #ffffff;
--ink: #2a1a2e;
--ink-2: #5a4a55;
--muted: #8a7a85;
--coral: #ff5e6c;
--coral-d: #e63950;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--pink: #ff8fb1;
--grad: linear-gradient(135deg, #ff5e6c, #8b5cf6);
--line: rgba(42, 26, 46, 0.10);
--white: #fff;
--r-sm: 12px;
--r-md: 20px;
--r-lg: 28px;
--shadow: 0 18px 50px rgba(124, 58, 237, 0.16);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1000px 600px at 15% -10%, rgba(255, 94, 108, 0.18), transparent 60%),
radial-gradient(900px 600px at 100% 0%, rgba(139, 92, 246, 0.16), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 28px 16px;
}
/* ---------- Phone frame ---------- */
.phone {
width: 100%;
max-width: 430px;
background: var(--surface);
border-radius: 40px;
box-shadow: var(--shadow), 0 0 0 10px rgba(255, 255, 255, 0.7);
border: 1px solid var(--line);
overflow: hidden;
display: flex;
flex-direction: column;
height: min(880px, calc(100vh - 56px));
position: relative;
}
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 16px 16px 12px;
border-bottom: 1px solid var(--line);
background: var(--surface);
z-index: 4;
}
.topbar-title {
font-size: 17px;
font-weight: 700;
margin: 0;
letter-spacing: -0.01em;
}
.icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--white);
color: var(--ink);
display: grid;
place-items: center;
cursor: pointer;
transition: transform .15s ease, background .2s ease, box-shadow .2s ease;
}
.icon-btn:hover { background: #fff0f2; transform: translateY(-1px); }
.icon-btn:active { transform: scale(.94); }
.preview-btn { color: var(--violet-d); }
:focus-visible {
outline: 3px solid rgba(139, 92, 246, 0.55);
outline-offset: 2px;
}
/* ---------- Scroll area ---------- */
.scroll {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 18px;
scrollbar-width: thin;
}
.scroll-pad { height: 4px; }
/* ---------- Identity ---------- */
.identity {
display: flex;
align-items: center;
gap: 14px;
}
.avatar {
width: 58px;
height: 58px;
border-radius: 50%;
padding: 3px;
background: var(--grad);
flex: none;
}
.avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--white);
display: block;
}
.name { margin: 0; font-weight: 700; font-size: 18px; }
.handle { margin: 2px 0 0; color: var(--muted); font-size: 13px; }
/* ---------- Completeness meter ---------- */
.meter-card {
background: linear-gradient(180deg, #fff, #fff8f9);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
}
.meter-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 10px;
}
.meter-label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.meter-pct {
font-weight: 800;
font-size: 18px;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-variant-numeric: tabular-nums;
}
.meter-track {
height: 10px;
border-radius: 999px;
background: rgba(42, 26, 46, 0.08);
overflow: hidden;
}
.meter-fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: var(--grad);
transition: width .5s cubic-bezier(.22, 1, .36, 1);
}
.meter-hint {
margin: 9px 0 0;
font-size: 12px;
color: var(--muted);
}
/* ---------- Blocks ---------- */
.block-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.block-title {
margin: 0;
font-size: 15px;
font-weight: 700;
letter-spacing: -0.01em;
}
.block-sub {
font-size: 12px;
font-weight: 600;
color: var(--violet-d);
background: rgba(139, 92, 246, 0.1);
padding: 3px 9px;
border-radius: 999px;
font-variant-numeric: tabular-nums;
}
.block-desc {
margin: 6px 0 10px;
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Photo grid ---------- */
.photo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.photo {
position: relative;
aspect-ratio: 3 / 4;
border-radius: var(--r-sm);
overflow: hidden;
cursor: grab;
user-select: none;
transition: transform .18s ease, box-shadow .2s ease, opacity .2s ease;
}
.photo img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.photo.filled { box-shadow: 0 6px 16px rgba(42, 26, 46, 0.14); }
.photo.filled:hover { transform: translateY(-2px); }
.photo.dragging {
opacity: .5;
transform: scale(.96);
cursor: grabbing;
}
.photo.drop-target {
outline: 2.5px dashed var(--violet);
outline-offset: -3px;
}
.photo .main-badge {
position: absolute;
top: 6px;
left: 6px;
font-size: 10px;
font-weight: 700;
color: #fff;
background: var(--grad);
padding: 3px 8px;
border-radius: 999px;
letter-spacing: .02em;
}
.photo .remove {
position: absolute;
top: 5px;
right: 5px;
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
background: rgba(42, 26, 46, 0.6);
color: #fff;
font-size: 14px;
line-height: 1;
cursor: pointer;
display: grid;
place-items: center;
backdrop-filter: blur(3px);
transition: background .18s ease, transform .15s ease;
}
.photo .remove:hover { background: var(--coral-d); transform: scale(1.08); }
.photo.empty {
cursor: pointer;
background: repeating-linear-gradient(135deg, #fff, #fff 10px, #fff5f6 10px, #fff5f6 20px);
border: 1.5px dashed rgba(139, 92, 246, 0.4);
display: grid;
place-items: center;
}
.photo.empty:hover { border-color: var(--violet); background: #fdf7ff; }
.photo.empty .plus-lg {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--grad);
color: #fff;
display: grid;
place-items: center;
font-size: 22px;
font-weight: 600;
line-height: 0;
}
/* ---------- Bio ---------- */
.bio-wrap { position: relative; }
.bio {
width: 100%;
resize: vertical;
min-height: 96px;
font-family: inherit;
font-size: 14px;
color: var(--ink);
line-height: 1.55;
border: 1.5px solid var(--line);
border-radius: var(--r-sm);
padding: 12px 14px;
background: #fffdfd;
transition: border-color .2s ease, box-shadow .2s ease;
}
.bio::placeholder { color: #b8a8b2; }
.bio:focus {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.14);
}
.bio-foot {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 7px;
}
.bio-tip { font-size: 12px; color: var(--muted); }
.bio-count {
font-size: 12px;
font-weight: 600;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.bio-count.warn { color: var(--coral-d); }
/* ---------- Chips ---------- */
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
font-family: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
border: 1.5px solid var(--line);
border-radius: 999px;
padding: 8px 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: transform .14s ease, border-color .2s ease, color .2s ease, background .2s ease, box-shadow .2s ease;
}
.chip .emoji { font-size: 14px; }
.chip:hover { border-color: var(--violet); transform: translateY(-1px); }
.chip:active { transform: scale(.95); }
.chip.on {
color: #fff;
background: var(--grad);
border-color: transparent;
box-shadow: 0 6px 16px rgba(230, 57, 80, 0.28);
}
.chip.disabled {
opacity: .45;
cursor: not-allowed;
pointer-events: none;
}
/* ---------- Prompts ---------- */
.prompts {
display: flex;
flex-direction: column;
gap: 10px;
}
.prompt-card {
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
background: #fffdfd;
transition: border-color .2s ease, box-shadow .2s ease;
}
.prompt-card:focus-within {
border-color: var(--violet);
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.12);
}
.prompt-q {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.prompt-q .q-text {
font-size: 13px;
font-weight: 700;
color: var(--violet-d);
cursor: pointer;
flex: 1;
}
.prompt-q .q-text:hover { text-decoration: underline; }
.prompt-remove {
border: none;
background: transparent;
color: var(--muted);
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 2px 4px;
border-radius: 6px;
transition: color .18s ease, background .18s ease;
}
.prompt-remove:hover { color: var(--coral-d); background: #ffeef0; }
.prompt-answer {
width: 100%;
border: none;
background: transparent;
font-family: inherit;
font-size: 14px;
color: var(--ink);
margin-top: 6px;
resize: none;
min-height: 22px;
line-height: 1.5;
}
.prompt-answer:focus { outline: none; }
.prompt-answer::placeholder { color: #b8a8b2; }
.add-prompt {
margin-top: 12px;
width: 100%;
border: 1.5px dashed rgba(139, 92, 246, 0.45);
background: #fdf7ff;
color: var(--violet-d);
font-family: inherit;
font-weight: 700;
font-size: 14px;
border-radius: var(--r-md);
padding: 13px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: background .2s ease, transform .14s ease, border-color .2s ease;
}
.add-prompt:hover { background: #f6ecff; border-color: var(--violet); }
.add-prompt:active { transform: scale(.98); }
.add-prompt:disabled { opacity: .5; cursor: not-allowed; }
.add-prompt .plus { font-size: 17px; }
/* ---------- Save bar ---------- */
.savebar {
display: flex;
gap: 10px;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid var(--line);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8px);
}
.btn-ghost,
.btn-primary {
font-family: inherit;
font-weight: 700;
font-size: 14.5px;
border-radius: 999px;
padding: 13px 18px;
cursor: pointer;
transition: transform .14s ease, box-shadow .22s ease, opacity .2s ease;
}
.btn-ghost {
flex: 0 0 auto;
border: 1.5px solid var(--line);
background: var(--white);
color: var(--ink-2);
}
.btn-ghost:hover { background: #fff0f2; }
.btn-primary {
flex: 1;
border: none;
color: #fff;
background: var(--grad);
box-shadow: 0 10px 22px rgba(230, 57, 80, 0.32);
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 14px 28px rgba(230, 57, 80, 0.4); }
.btn-primary:active { transform: scale(.98); }
.btn-ghost:active { transform: scale(.97); }
/* ---------- Bottom sheet ---------- */
.sheet-scrim {
position: fixed;
inset: 0;
background: rgba(42, 26, 46, 0.45);
z-index: 20;
animation: fade .2s ease;
}
.sheet {
position: fixed;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: 100%;
max-width: 430px;
background: var(--surface);
border-radius: var(--r-lg) var(--r-lg) 0 0;
padding: 10px 18px calc(20px + env(safe-area-inset-bottom));
z-index: 21;
box-shadow: 0 -18px 50px rgba(42, 26, 46, 0.24);
animation: slideUp .28s cubic-bezier(.22, 1, .36, 1);
max-height: 72vh;
display: flex;
flex-direction: column;
}
.sheet-handle {
width: 44px;
height: 5px;
border-radius: 999px;
background: var(--line);
margin: 4px auto 10px;
}
.sheet-title {
margin: 0 0 12px;
font-size: 17px;
font-weight: 700;
}
.sheet-list {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.sheet-item {
text-align: left;
border: 1.5px solid var(--line);
background: #fffdfd;
font-family: inherit;
font-size: 14px;
font-weight: 600;
color: var(--ink);
border-radius: var(--r-sm);
padding: 13px 14px;
cursor: pointer;
transition: border-color .18s ease, background .18s ease, transform .12s ease;
}
.sheet-item:hover { border-color: var(--violet); background: #fdf7ff; }
.sheet-item:active { transform: scale(.99); }
.sheet-item.used {
opacity: .45;
pointer-events: none;
}
.sheet-close { width: 100%; text-align: center; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 92px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 12px 30px rgba(42, 26, 46, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity .25s ease, transform .25s ease;
z-index: 40;
max-width: 360px;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translate(-50%, 100%); } to { transform: translate(-50%, 0); } }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body { padding: 0; }
.phone {
max-width: 100%;
height: 100vh;
min-height: 100vh;
border-radius: 0;
box-shadow: none;
border: none;
}
.photo-grid { gap: 8px; }
.btn-ghost, .btn-primary { padding: 12px 16px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var STOCK = [
"https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&w=400&q=70",
"https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=400&q=70",
"https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&w=400&q=70",
"https://images.unsplash.com/photo-1502823403499-6ccfcf4fb453?auto=format&fit=crop&w=400&q=70",
"https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?auto=format&fit=crop&w=400&q=70"
];
// 6 photo slots; null = empty
var photos = [
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=400&q=70",
null, null, null, null, null
];
var INTERESTS = [
{ id: "ceramics", label: "Ceramics", emoji: "🏺" },
{ id: "hiking", label: "Hiking", emoji: "🥾" },
{ id: "sourdough", label: "Sourdough", emoji: "🍞" },
{ id: "vinyl", label: "Vinyl", emoji: "🎶" },
{ id: "coffee", label: "Specialty coffee", emoji: "☕" },
{ id: "climbing", label: "Bouldering", emoji: "🧗" },
{ id: "film", label: "Film photography", emoji: "📷" },
{ id: "travel", label: "Slow travel", emoji: "🌍" },
{ id: "cats", label: "Cats", emoji: "🐈" },
{ id: "gaming", label: "Co-op games", emoji: "🎮" },
{ id: "wine", label: "Natural wine", emoji: "🍷" },
{ id: "yoga", label: "Yoga", emoji: "🧘" }
];
var selectedInterests = ["ceramics", "coffee"];
var MAX_INTERESTS = 6;
var PROMPT_BANK = [
"My most irrational fear is…",
"The way to win me over is…",
"A shower thought I recently had…",
"Two truths and a lie…",
"My simple pleasures are…",
"I geek out about…",
"We'll get along if…",
"The last thing I saved to my camera roll…",
"My love language is…",
"Green flags I look for…"
];
// prompt: {q, a}
var prompts = [
{ q: "I geek out about…", a: "The exact water temperature for a V60 pour-over. It's a problem." }
];
var MAX_PROMPTS = 3;
/* ---------- Elements ---------- */
var $ = function (id) { return document.getElementById(id); };
var photoGrid = $("photoGrid");
var chipsWrap = $("chips");
var promptsWrap = $("prompts");
var bio = $("bio");
var toastEl = $("toast");
/* ---------- Toast ---------- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ---------- Photos: render + drag reorder ---------- */
var dragFrom = null;
function renderPhotos() {
photoGrid.innerHTML = "";
photos.forEach(function (src, i) {
var cell = document.createElement("div");
cell.className = "photo " + (src ? "filled" : "empty");
cell.setAttribute("data-index", i);
if (src) {
cell.setAttribute("draggable", "true");
cell.setAttribute("aria-label", "Photo " + (i + 1) + ", drag to reorder");
cell.innerHTML =
'<img src="' + src + '" alt="Profile photo ' + (i + 1) + '" />' +
(i === 0 ? '<span class="main-badge">MAIN</span>' : "") +
'<button class="remove" type="button" aria-label="Remove photo ' + (i + 1) + '">×</button>';
} else {
cell.setAttribute("role", "button");
cell.setAttribute("tabindex", "0");
cell.setAttribute("aria-label", "Add a photo");
cell.innerHTML = '<span class="plus-lg">+</span>';
}
photoGrid.appendChild(cell);
});
updatePhotoCount();
}
function firstEmptyIndex() {
for (var i = 0; i < photos.length; i++) { if (!photos[i]) return i; }
return -1;
}
function updatePhotoCount() {
var n = photos.filter(Boolean).length;
$("photoCount").textContent = n + " / 6";
}
// Delegated events on the grid
photoGrid.addEventListener("click", function (e) {
var cell = e.target.closest(".photo");
if (!cell) return;
var idx = +cell.getAttribute("data-index");
if (e.target.closest(".remove")) {
photos[idx] = null;
compactPhotos();
renderPhotos();
recompute();
toast("Photo removed");
return;
}
if (cell.classList.contains("empty")) {
addPhoto(idx);
}
});
photoGrid.addEventListener("keydown", function (e) {
var cell = e.target.closest(".photo.empty");
if (cell && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
addPhoto(+cell.getAttribute("data-index"));
}
});
function addPhoto(idx) {
// pick a stock image not already used
var used = photos.filter(Boolean);
var pick = STOCK.find(function (s) { return used.indexOf(s) === -1; }) || STOCK[0];
photos[idx] = pick;
renderPhotos();
recompute();
toast("Photo added — drag to reorder");
}
function compactPhotos() {
var filled = photos.filter(Boolean);
while (filled.length < 6) filled.push(null);
photos = filled;
}
// Drag & drop reorder (HTML5)
photoGrid.addEventListener("dragstart", function (e) {
var cell = e.target.closest(".photo.filled");
if (!cell) { e.preventDefault(); return; }
dragFrom = +cell.getAttribute("data-index");
cell.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/plain", String(dragFrom)); } catch (err) {}
});
photoGrid.addEventListener("dragend", function () {
dragFrom = null;
Array.prototype.forEach.call(photoGrid.children, function (c) {
c.classList.remove("dragging", "drop-target");
});
});
photoGrid.addEventListener("dragover", function (e) {
var cell = e.target.closest(".photo");
if (dragFrom === null || !cell) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
Array.prototype.forEach.call(photoGrid.children, function (c) { c.classList.remove("drop-target"); });
if (+cell.getAttribute("data-index") !== dragFrom) cell.classList.add("drop-target");
});
photoGrid.addEventListener("drop", function (e) {
var cell = e.target.closest(".photo");
if (dragFrom === null || !cell) return;
e.preventDefault();
var to = +cell.getAttribute("data-index");
if (to === dragFrom) return;
var moved = photos.splice(dragFrom, 1)[0];
photos.splice(to, 0, moved);
compactPhotos();
renderPhotos();
recompute();
toast(to === 0 ? "New main photo set" : "Photos reordered");
});
/* ---------- Bio counter ---------- */
bio.value = "Frontend dev by day, ceramics disaster by weekend. Currently training a sourdough starter named Kevin and losing.";
function updateBio() {
var len = bio.value.length;
var max = +bio.getAttribute("maxlength");
var el = $("bioCount");
el.textContent = len + " / " + max;
el.classList.toggle("warn", len > max - 40);
}
bio.addEventListener("input", function () { updateBio(); recompute(); });
/* ---------- Interest chips ---------- */
function renderChips() {
chipsWrap.innerHTML = "";
var atMax = selectedInterests.length >= MAX_INTERESTS;
INTERESTS.forEach(function (it) {
var on = selectedInterests.indexOf(it.id) !== -1;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "chip" + (on ? " on" : "") + (!on && atMax ? " disabled" : "");
btn.setAttribute("aria-pressed", on ? "true" : "false");
btn.setAttribute("data-id", it.id);
btn.innerHTML = '<span class="emoji" aria-hidden="true">' + it.emoji + "</span>" + it.label;
chipsWrap.appendChild(btn);
});
$("chipCount").textContent = selectedInterests.length + " / " + MAX_INTERESTS;
}
chipsWrap.addEventListener("click", function (e) {
var btn = e.target.closest(".chip");
if (!btn) return;
var id = btn.getAttribute("data-id");
var pos = selectedInterests.indexOf(id);
if (pos !== -1) {
selectedInterests.splice(pos, 1);
} else {
if (selectedInterests.length >= MAX_INTERESTS) {
toast("You can pick up to " + MAX_INTERESTS + " interests");
return;
}
selectedInterests.push(id);
}
renderChips();
recompute();
});
/* ---------- Prompts ---------- */
function renderPrompts() {
promptsWrap.innerHTML = "";
prompts.forEach(function (p, i) {
var card = document.createElement("div");
card.className = "prompt-card";
card.innerHTML =
'<div class="prompt-q">' +
'<span class="q-text" role="button" tabindex="0" data-change="' + i + '">' + p.q + "</span>" +
'<button class="prompt-remove" type="button" data-remove="' + i + '" aria-label="Remove prompt">×</button>' +
"</div>" +
'<textarea class="prompt-answer" data-answer="' + i + '" rows="1" maxlength="180" ' +
'placeholder="Tap to write your answer…">' + escapeHtml(p.a) + "</textarea>";
promptsWrap.appendChild(card);
autoGrow(card.querySelector(".prompt-answer"));
});
$("promptCount").textContent = prompts.length + " / " + MAX_PROMPTS;
$("addPrompt").disabled = prompts.length >= MAX_PROMPTS;
}
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function autoGrow(ta) {
ta.style.height = "auto";
ta.style.height = (ta.scrollHeight) + "px";
}
promptsWrap.addEventListener("input", function (e) {
if (e.target.matches(".prompt-answer")) {
var i = +e.target.getAttribute("data-answer");
prompts[i].a = e.target.value;
autoGrow(e.target);
recompute();
}
});
promptsWrap.addEventListener("click", function (e) {
var rm = e.target.closest("[data-remove]");
if (rm) {
prompts.splice(+rm.getAttribute("data-remove"), 1);
renderPrompts();
recompute();
toast("Prompt removed");
return;
}
var ch = e.target.closest("[data-change]");
if (ch) openSheet(+ch.getAttribute("data-change"));
});
promptsWrap.addEventListener("keydown", function (e) {
var ch = e.target.closest("[data-change]");
if (ch && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
openSheet(+ch.getAttribute("data-change"));
}
});
$("addPrompt").addEventListener("click", function () {
if (prompts.length >= MAX_PROMPTS) return;
openSheet(-1); // -1 = add new
});
/* ---------- Prompt picker sheet ---------- */
var sheet = $("sheet"), scrim = $("sheetScrim"), sheetList = $("sheetList");
var editingIndex = null;
function openSheet(index) {
editingIndex = index;
var usedQs = prompts.map(function (p) { return p.q; });
sheetList.innerHTML = "";
PROMPT_BANK.forEach(function (q) {
var used = usedQs.indexOf(q) !== -1 && !(index >= 0 && prompts[index].q === q);
var b = document.createElement("button");
b.type = "button";
b.className = "sheet-item" + (used ? " used" : "");
b.textContent = q;
b.setAttribute("data-q", q);
sheetList.appendChild(b);
});
scrim.hidden = false;
sheet.hidden = false;
document.addEventListener("keydown", onSheetKey);
}
function closeSheet() {
sheet.hidden = true;
scrim.hidden = true;
editingIndex = null;
document.removeEventListener("keydown", onSheetKey);
}
function onSheetKey(e) { if (e.key === "Escape") closeSheet(); }
sheetList.addEventListener("click", function (e) {
var item = e.target.closest(".sheet-item");
if (!item) return;
var q = item.getAttribute("data-q");
if (editingIndex === -1) {
prompts.push({ q: q, a: "" });
toast("Prompt added — write your answer");
} else {
prompts[editingIndex].q = q;
toast("Prompt swapped");
}
closeSheet();
renderPrompts();
recompute();
});
scrim.addEventListener("click", closeSheet);
$("sheetClose").addEventListener("click", closeSheet);
/* ---------- Completeness meter ---------- */
function recompute() {
var parts = [];
// photos: 30 pts, scaled 1..4+
var pc = photos.filter(Boolean).length;
parts.push(Math.min(pc / 4, 1) * 30);
// bio: 25 pts (needs ~60 chars to max)
parts.push(Math.min(bio.value.trim().length / 60, 1) * 25);
// interests: 20 pts (needs 4)
parts.push(Math.min(selectedInterests.length / 4, 1) * 20);
// prompts: 25 pts (needs 3 answered)
var answered = prompts.filter(function (p) { return p.a.trim().length > 4; }).length;
parts.push(Math.min(answered / 3, 1) * 25);
var pct = Math.round(parts.reduce(function (a, b) { return a + b; }, 0));
if (pct > 100) pct = 100;
$("meterPct").textContent = pct + "%";
$("meterFill").style.width = pct + "%";
var bar = $("meterBar");
bar.setAttribute("aria-valuenow", String(pct));
var hint;
if (pct >= 100) hint = "Looking great — your profile is complete! ✨";
else if (pct >= 75) hint = "Almost there — add one more thing to shine.";
else if (pct >= 45) hint = "Nice start. More photos and prompts get more matches.";
else hint = "Add a few more details to stand out.";
$("meterHint").textContent = hint;
}
/* ---------- Save / discard ---------- */
var pristine;
function snapshot() {
return JSON.stringify({
photos: photos, bio: bio.value,
interests: selectedInterests, prompts: prompts
});
}
$("saveBtn").addEventListener("click", function () {
pristine = snapshot();
toast("Profile saved 💜");
});
$("discardBtn").addEventListener("click", function () {
if (snapshot() === pristine) { toast("No changes to discard"); return; }
var s = JSON.parse(pristine);
photos = s.photos.slice();
bio.value = s.bio;
selectedInterests = s.interests.slice();
prompts = s.prompts.map(function (p) { return { q: p.q, a: p.a }; });
renderAll();
toast("Changes discarded");
});
document.querySelector(".preview-btn").addEventListener("click", function () {
toast("Preview mode — this is how others see you");
});
/* ---------- Init ---------- */
function renderAll() {
renderPhotos();
renderChips();
renderPrompts();
updateBio();
recompute();
}
renderAll();
pristine = snapshot();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Edit Profile — Spark</title>
<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;700;800&family=Fraunces:opsz,[email protected],600;9..144,700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="phone" role="application" aria-label="Edit dating profile">
<!-- Status / top bar -->
<header class="topbar">
<button class="icon-btn" type="button" aria-label="Go back">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<h1 class="topbar-title">Edit profile</h1>
<button class="icon-btn preview-btn" type="button" aria-label="Preview profile" title="Preview">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2"/></svg>
</button>
</header>
<div class="scroll">
<!-- Identity + completeness -->
<section class="identity">
<div class="avatar" aria-hidden="true">
<img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=160&q=70" alt="" />
</div>
<div class="identity-body">
<p class="name">Maya, 27</p>
<p class="handle">@maya.codes · Lisbon</p>
</div>
</section>
<section class="meter-card" aria-live="polite">
<div class="meter-head">
<span class="meter-label">Profile completeness</span>
<span class="meter-pct" id="meterPct">0%</span>
</div>
<div class="meter-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="meterBar">
<div class="meter-fill" id="meterFill"></div>
</div>
<p class="meter-hint" id="meterHint">Add a few more details to stand out.</p>
</section>
<!-- Photos -->
<section class="block">
<div class="block-head">
<h2 class="block-title">Photos</h2>
<span class="block-sub" id="photoCount">1 / 6</span>
</div>
<p class="block-desc">Drag to reorder. Your first photo is the one people see first.</p>
<div class="photo-grid" id="photoGrid"></div>
</section>
<!-- Bio -->
<section class="block">
<div class="block-head">
<h2 class="block-title">About me</h2>
</div>
<div class="bio-wrap">
<textarea id="bio" class="bio" maxlength="300" rows="4"
placeholder="Frontend dev by day, ceramics disaster by weekend. Ask me about my sourdough starter named Kevin."></textarea>
<div class="bio-foot">
<span class="bio-tip">Be specific — it sparks better chats.</span>
<span class="bio-count" id="bioCount">0 / 300</span>
</div>
</div>
</section>
<!-- Interests -->
<section class="block">
<div class="block-head">
<h2 class="block-title">Interests</h2>
<span class="block-sub" id="chipCount">0 / 6</span>
</div>
<p class="block-desc">Pick up to 6 things you love talking about.</p>
<div class="chips" id="chips"></div>
</section>
<!-- Prompts -->
<section class="block">
<div class="block-head">
<h2 class="block-title">Prompts</h2>
<span class="block-sub" id="promptCount">0 / 3</span>
</div>
<p class="block-desc">Answer a few to give people something to reply to.</p>
<div class="prompts" id="prompts"></div>
<button class="add-prompt" type="button" id="addPrompt">
<span class="plus">+</span> Add a prompt
</button>
</section>
<div class="scroll-pad"></div>
</div>
<!-- Save bar -->
<footer class="savebar">
<button class="btn-ghost" type="button" id="discardBtn">Discard</button>
<button class="btn-primary" type="button" id="saveBtn">Save changes</button>
</footer>
</div>
<!-- Prompt picker sheet -->
<div class="sheet-scrim" id="sheetScrim" hidden></div>
<aside class="sheet" id="sheet" role="dialog" aria-modal="true" aria-labelledby="sheetTitle" hidden>
<div class="sheet-handle" aria-hidden="true"></div>
<h2 class="sheet-title" id="sheetTitle">Choose a prompt</h2>
<div class="sheet-list" id="sheetList"></div>
<button class="btn-ghost sheet-close" type="button" id="sheetClose">Cancel</button>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>An edit-profile screen for a dating app, framed like a real phone held in the hand. The top of the card shows a six-slot photo grid where the first slot is your main shot and empty slots invite an upload; drag any tile onto another to reorder, and the layout reflows with a soft lift-and-drop micro-interaction. Below that a bio field tracks its own character count and warns as you approach the limit.
Interests are a wrapping row of pill chips you toggle on and off, capped so the profile stays tidy. The prompts section lets you pick from a curated list, type an answer, and swap prompts whenever you like — each answered prompt reads back like a little card on your profile. Every meaningful edit nudges a completeness meter at the top, which changes label and color as you approach one hundred percent.
Everything is vanilla JS: no frameworks, no build step. State lives in plain arrays, drag reordering uses the native HTML5 drag events, and a small toast helper surfaces friendly confirmations. Styles are fully responsive down to about 360px and honor keyboard focus and reduced-motion preferences.