Compare commits

...

2 commits

Author SHA1 Message Date
1c3273cee2 Various pre-launch fixes.
Some checks failed
Test / vitest (push) Successful in 14m0s
Test / playwright (push) Failing after 20m2s
Test / Notify on failure (push) Successful in 3s
2026-05-22 18:53:07 +01:00
246f2023bc Add a sweet ghostie favicon 2026-05-22 14:58:57 +01:00
10 changed files with 30 additions and 39 deletions

View file

@ -88,19 +88,7 @@ defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete'])
const { slackUrl } = useBoardChannels()
const capitalizeAvatar = (str) => {
if (str.toLowerCase() === 'wtf') return 'WTF'
return str
.split('-')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join('-')
}
const authorAvatar = computed(() => {
const a = props.post.author?.avatar
if (!a) return null
return `/ghosties/Ghost-${capitalizeAvatar(a)}.png`
})
const authorAvatar = computed(() => ghostieImagePath(props.post.author?.avatar))
const slackHandle = computed(() => props.post.author?.board?.slackHandle || '')

View file

@ -33,8 +33,7 @@
</dl>
</DashedBox>
<p class="signup-flow-body" style="margin-top: 16px">
Check {{ summary?.email }} for a sign-in link to finish setting up
your account. The link expires in 15 minutes.
{{ successMessage || `Check ${summary?.email} for a sign-in link to finish setting up your account. The link expires in 15 minutes.` }}
</p>
</template>
@ -62,6 +61,7 @@ const props = defineProps({
summary: { type: Object, default: null },
errorMessage: { type: String, default: "" },
dashboardHref: { type: String, default: "/welcome" },
successMessage: { type: String, default: "" },
});
defineEmits(["close"]);

View file

@ -29,7 +29,7 @@
<NuxtLink to="/member/profile" class="member-link">
<img
v-if="memberData.avatar"
:src="`/ghosties/Ghost-${capitalize(memberData.avatar)}.png`"
:src="ghostieImagePath(memberData.avatar)"
:alt="memberData.name"
class="member-avatar"
>
@ -86,11 +86,6 @@ const handleLogout = async () => {
navigateTo("/");
};
const capitalize = (str) => {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
};
const breadcrumbs = computed(() => {
if (!props.pagePath) return [];
const segments = props.pagePath.split(" / ");

View file

@ -229,6 +229,7 @@
:summary="flowSummary"
:error-message="errorMessage"
dashboard-href="/member/dashboard?welcome=1"
success-message="You're signed in. Taking you to your dashboard..."
@close="closeFlowOverlay"
/>
</div>

View file

@ -23,7 +23,7 @@
<div class="profile-avatar">
<img
v-if="member.avatar"
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`"
:src="ghostieImagePath(member.avatar)"
:alt="member.name"
class="profile-avatar-img"
/>

View file

@ -89,7 +89,7 @@
<div class="mc-avatar">
<img
v-if="member.avatar"
:src="`/ghosties/Ghost-${capitalize(member.avatar)}.png`"
:src="ghostieImagePath(member.avatar)"
:alt="member.name"
class="mc-avatar-img"
>
@ -185,14 +185,6 @@ const getInitials = (name) => {
.slice(0, 2)
}
const capitalize = (str) => {
if (!str) return ''
return str
.split('-')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join('-')
}
// ---- Computed ----
const visibleTagOptions = computed(() =>
showAllTags.value ? craftTagOptions.value : craftTagOptions.value.slice(0, 10)

View file

@ -0,0 +1,15 @@
const AVATAR_FILE_NAMES = {
'sweet': 'Sweet',
'mild': 'Mild',
'exasperated': 'Exasperated',
'disbelieving': 'Disbelieving',
'double-take': 'Double-Take',
'wtf': 'WTF',
}
export function ghostieImagePath(avatar) {
if (!avatar) return null
const name = AVATAR_FILE_NAMES[String(avatar).toLowerCase()]
if (!name) return null
return `/ghosties/Ghost-${name}.png`
}

View file

@ -23,6 +23,7 @@ export default defineNuxtConfig({
title: "Ghost Guild",
titleTemplate: "%s · Ghost Guild",
link: [
{ rel: "icon", type: "image/svg+xml", href: "/ghosties/Ghost-Sweet.svg" },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",

View file

@ -68,12 +68,14 @@ export default defineEventHandler(async (event) => {
await assignMemberNumber(member._id)
// Update pre-registration
// Update pre-registration. Burning the jti here (instead of in verify) keeps
// the verify endpoint idempotent so a page refresh doesn't lock the invitee out.
await PreRegistration.findByIdAndUpdate(preReg._id, {
$set: {
status: 'accepted',
acceptedAt: new Date(),
memberId: member._id,
magicLinkJtiUsed: true,
}
})

View file

@ -27,16 +27,13 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, statusMessage: 'This invitation has already been accepted' })
}
// Single-use enforcement
if (!decoded.jti || decoded.jti !== preReg.magicLinkJti || preReg.magicLinkJtiUsed) {
// Match the jti so that re-invite (which rotates the jti) kills old links.
// The burn happens in accept.post.js once a Member is created — keeps verify
// idempotent so the form survives a refresh.
if (!decoded.jti || decoded.jti !== preReg.magicLinkJti) {
throw createError({ statusCode: 401, statusMessage: 'Invalid or expired invitation link' })
}
// Burn the token
await PreRegistration.findByIdAndUpdate(preReg._id, {
$set: { magicLinkJtiUsed: true }
})
return {
preRegistrationId: preReg._id,
name: preReg.name,