refactor(account): migrate member/account to PageShell vocabulary

- Replace .member-account-page + .account-authenticated flex chains with <PageShell>
- Replace <SidebarLayout> with <ColumnsLayout cols=events-sidebar>
- Replace .account-columns grid with nested <ColumnsLayout cols=2> + named slots
- Replace all 5 .account-col-inset wrappers with <PageSection> components
- Rename .account-section--danger → .danger-section
- Delete ~75 lines of layout CSS (flex chains, grid, asymmetric paddings, collapse rules)
This commit is contained in:
Jennie Robinson Faber 2026-04-08 17:07:30 +01:00
parent f267b35214
commit 37fceac3fd

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="member-account-page"> <PageShell>
<!-- Unauthenticated --> <!-- Unauthenticated -->
<div v-if="!memberData" class="loading"> <div v-if="!memberData" class="loading">
<p>Please sign in to access your account settings.</p> <p>Please sign in to access your account settings.</p>
@ -11,7 +11,7 @@
</button> </button>
</div> </div>
<div v-else class="account-authenticated"> <template v-else>
<!-- PAGE HEADER --> <!-- PAGE HEADER -->
<PageHeader <PageHeader
title="Account Settings" title="Account Settings"
@ -19,189 +19,179 @@
/> />
<!-- CONTENT AREA WITH EVENTS SIDEBAR --> <!-- CONTENT AREA WITH EVENTS SIDEBAR -->
<SidebarLayout> <ColumnsLayout cols="events-sidebar">
<div class="account-columns"> <ColumnsLayout cols="2">
<!-- LEFT COLUMN: Membership Status & Email --> <!-- LEFT COLUMN: Membership Status & Email -->
<div class="account-col-left"> <template #left>
<section class="account-section"> <PageSection>
<div class="account-col-inset"> <div class="section-label">Current Membership</div>
<div class="section-label">Current Membership</div>
<div class="membership-card"> <div class="membership-card">
<div class="membership-row"> <div class="membership-row">
<span class="membership-k">Status</span> <span class="membership-k">Status</span>
<span class="membership-v status-v"> <span class="membership-v status-v">
<span
class="status-dot"
:class="memberData.status || 'active'"
></span>
<span>{{
formatStatus(memberData.status || "active")
}}</span>
</span>
</div>
<div class="membership-row">
<span class="membership-k">Circle</span>
<span <span
class="membership-v" class="status-dot"
:style="{ :class="memberData.status || 'active'"
color: `var(--c-${memberData.circle || 'community'})`, ></span>
}" <span>{{
> formatStatus(memberData.status || "active")
{{
memberData.circle
? capitalise(memberData.circle)
: "Community"
}}
</span>
</div>
<div class="membership-row">
<span class="membership-k">Contribution</span>
<span class="membership-v"
>${{ memberData.contributionTier || 0 }} / month</span
>
</div>
<div class="membership-row">
<span class="membership-k">Member since</span>
<span class="membership-v">{{
formatMemberSince(memberData.createdAt)
}}</span> }}</span>
</div> </span>
</div> </div>
</div> <div class="membership-row">
</section> <span class="membership-k">Circle</span>
<span
<section class="account-section"> class="membership-v"
<div class="account-col-inset"> :style="{
<div class="section-label">Email</div> color: `var(--c-${memberData.circle || 'community'})`,
}"
<div v-if="!showEmailEdit" class="email-display">
<span class="email-value">{{ memberData.email }}</span>
<button class="btn btn-inline" @click="showEmailEdit = true">
Change
</button>
</div>
<div v-else class="email-edit">
<div class="field">
<label>New email address</label>
<input
type="email"
v-model="newEmail"
placeholder="you@example.com"
@keydown.enter="handleUpdateEmail"
@keydown.escape="cancelEmailEdit"
autofocus
/>
</div>
<div class="email-edit-actions">
<button
class="btn btn-primary"
@click="handleUpdateEmail"
:disabled="isUpdatingEmail || !newEmail.trim()"
>
{{ isUpdatingEmail ? "Saving…" : "Save" }}
</button>
<button
class="btn"
@click="cancelEmailEdit"
:disabled="isUpdatingEmail"
>
Cancel
</button>
</div>
</div>
<div class="email-hint">
Used for login magic links and notifications
</div>
</div>
</section>
<section class="account-section account-section--danger">
<div class="account-col-inset">
<div class="section-label danger">Danger Zone</div>
<div class="danger-zone">
<p>
Cancelling your membership will immediately revoke access to
member-only resources, events, and the Slack workspace.
<strong>This action cannot be easily undone.</strong>
</p>
<div v-if="showCancelConfirm" class="cancel-confirm">
<p class="cancel-confirm-prompt">
Are you sure? This cannot be easily undone.
</p>
<div class="cancel-confirm-actions">
<button
class="btn btn-danger"
@click="confirmCancelMembership"
:disabled="isCancelling"
>
{{ isCancelling ? "Cancelling…" : "Yes, Cancel" }}
</button>
<button class="btn" @click="showCancelConfirm = false">
Nevermind
</button>
</div>
</div>
<button
v-else
class="btn btn-danger"
@click="handleCancelMembership"
:disabled="isCancelling"
> >
Cancel Membership {{
memberData.circle
? capitalise(memberData.circle)
: "Community"
}}
</span>
</div>
<div class="membership-row">
<span class="membership-k">Contribution</span>
<span class="membership-v"
>${{ memberData.contributionTier || 0 }} / month</span
>
</div>
<div class="membership-row">
<span class="membership-k">Member since</span>
<span class="membership-v">{{
formatMemberSince(memberData.createdAt)
}}</span>
</div>
</div>
</PageSection>
<PageSection divider="top">
<div class="section-label">Email</div>
<div v-if="!showEmailEdit" class="email-display">
<span class="email-value">{{ memberData.email }}</span>
<button class="btn btn-inline" @click="showEmailEdit = true">
Change
</button>
</div>
<div v-else class="email-edit">
<div class="field">
<label>New email address</label>
<input
type="email"
v-model="newEmail"
placeholder="you@example.com"
@keydown.enter="handleUpdateEmail"
@keydown.escape="cancelEmailEdit"
autofocus
/>
</div>
<div class="email-edit-actions">
<button
class="btn btn-primary"
@click="handleUpdateEmail"
:disabled="isUpdatingEmail || !newEmail.trim()"
>
{{ isUpdatingEmail ? "Saving…" : "Save" }}
</button>
<button
class="btn"
@click="cancelEmailEdit"
:disabled="isUpdatingEmail"
>
Cancel
</button> </button>
</div> </div>
</div> </div>
</section>
</div> <div class="email-hint">
Used for login magic links and notifications
</div>
</PageSection>
<PageSection divider="top" class="danger-section">
<div class="section-label danger">Danger Zone</div>
<div class="danger-zone">
<p>
Cancelling your membership will immediately revoke access to
member-only resources, events, and the Slack workspace.
<strong>This action cannot be easily undone.</strong>
</p>
<div v-if="showCancelConfirm" class="cancel-confirm">
<p class="cancel-confirm-prompt">
Are you sure? This cannot be easily undone.
</p>
<div class="cancel-confirm-actions">
<button
class="btn btn-danger"
@click="confirmCancelMembership"
:disabled="isCancelling"
>
{{ isCancelling ? "Cancelling…" : "Yes, Cancel" }}
</button>
<button class="btn" @click="showCancelConfirm = false">
Nevermind
</button>
</div>
</div>
<button
v-else
class="btn btn-danger"
@click="handleCancelMembership"
:disabled="isCancelling"
>
Cancel Membership
</button>
</div>
</PageSection>
</template>
<!-- RIGHT COLUMN: Change Contribution & Circle --> <!-- RIGHT COLUMN: Change Contribution & Circle -->
<div class="account-col-right"> <template #right>
<section class="account-section"> <PageSection>
<div class="account-col-inset"> <div class="section-label">Change Contribution</div>
<div class="section-label">Change Contribution</div>
<TierPicker v-model="selectedTier" :tiers="tiers" /> <TierPicker v-model="selectedTier" :tiers="tiers" />
<div class="tier-hint"> <div class="tier-hint">
Changes take effect on your next billing cycle Changes take effect on your next billing cycle
</div>
<button
class="btn btn-primary btn-section"
@click="handleUpdateTier"
:disabled="
selectedTier === Number(memberData.contributionTier || 0) ||
isUpdating
"
>
{{ isUpdating ? "Updating…" : "Update Contribution" }}
</button>
</div> </div>
</section> <button
class="btn btn-primary btn-section"
@click="handleUpdateTier"
:disabled="
selectedTier === Number(memberData.contributionTier || 0) ||
isUpdating
"
>
{{ isUpdating ? "Updating…" : "Update Contribution" }}
</button>
</PageSection>
<section class="account-section"> <PageSection divider="top">
<div class="account-col-inset"> <div class="section-label">Change Circle</div>
<div class="section-label">Change Circle</div>
<CirclePicker <CirclePicker
v-model="selectedCircle" v-model="selectedCircle"
:circles="circleOptions" :circles="circleOptions"
/> />
<button <button
class="btn btn-primary btn-section" class="btn btn-primary btn-section"
@click="handleUpdateCircle" @click="handleUpdateCircle"
:disabled="selectedCircle === memberData.circle || isUpdating" :disabled="selectedCircle === memberData.circle || isUpdating"
> >
{{ isUpdating ? "Updating…" : "Update Circle" }} {{ isUpdating ? "Updating…" : "Update Circle" }}
</button> </button>
</div> </PageSection>
</section> </template>
</div> </ColumnsLayout>
</div> </ColumnsLayout>
</SidebarLayout> </template>
</div> </PageShell>
</div>
</template> </template>
<script setup> <script setup>
@ -385,82 +375,11 @@ const confirmCancelMembership = async () => {
</script> </script>
<style scoped> <style scoped>
.member-account-page {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.loading { .loading {
flex: 1;
padding: 48px 32px; padding: 48px 32px;
color: var(--text-dim); color: var(--text-dim);
} }
.account-authenticated {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* ---- TWO-COLUMN LAYOUT ---- */
.account-columns {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
align-items: stretch;
min-height: 0;
}
.account-col-left,
.account-col-right {
display: flex;
flex-direction: column;
min-height: 0;
align-self: stretch;
width: 100%;
min-width: 0;
}
.account-col-left {
border-right: 1px dashed var(--border);
}
/* Full-column rules: border on block-level section */
.account-section {
width: 100%;
min-width: 0;
}
.account-section + .account-section {
margin-top: 24px;
border-top: 1px dashed var(--border);
padding-top: 20px;
}
.account-section + .account-section.account-section--danger {
margin-top: 24px;
padding-top: 20px;
}
.account-col-left > .account-section:first-child .account-col-inset,
.account-col-right > .account-section:first-child .account-col-inset {
padding-top: 24px;
}
.account-col-left > .account-section:last-child .account-col-inset,
.account-col-right > .account-section:last-child .account-col-inset {
padding-bottom: 24px;
}
.account-col-left .account-col-inset {
padding-left: 28px;
padding-right: 24px;
}
.account-col-right .account-col-inset {
padding-left: 24px;
padding-right: 28px;
}
/* ---- MEMBERSHIP CARD ---- */ /* ---- MEMBERSHIP CARD ---- */
.membership-card { .membership-card {
border: 1px dashed var(--border); border: 1px dashed var(--border);
@ -567,14 +486,14 @@ const confirmCancelMembership = async () => {
} }
/* ---- DANGER ZONE ---- */ /* ---- DANGER ZONE ---- */
.account-section--danger { .danger-section {
background: var(--ember-bg); background: var(--ember-bg);
} }
.account-section--danger .section-label.danger { .danger-section .section-label.danger {
color: var(--ember); color: var(--ember);
} }
.account-section--danger .danger-zone p { .danger-section .danger-zone p {
color: var(--text-dim); color: var(--text-dim);
font-size: 12px; font-size: 12px;
line-height: 1.7; line-height: 1.7;
@ -610,19 +529,4 @@ const confirmCancelMembership = async () => {
text-align: center; text-align: center;
} }
/* ---- RESPONSIVE ---- */
@media (max-width: 1024px) {
.account-columns {
grid-template-columns: 1fr;
}
.account-col-left {
border-right: none;
border-bottom: 1px dashed var(--border);
}
.account-col-left .account-col-inset,
.account-col-right .account-col-inset {
padding-left: 28px;
padding-right: 28px;
}
}
</style> </style>