Accessibility fixes.

This commit is contained in:
Jennie Robinson Faber 2026-04-05 19:27:25 +01:00
parent 689548e389
commit dae983734a
7 changed files with 201 additions and 140 deletions

View file

@ -30,6 +30,30 @@ export default defineNuxtConfig({
hmr: { hmr: {
port: 24678, port: 24678,
}, },
watch: {
ignored: [
"**/.git/**",
"**/.nuxt/**",
"**/.output/**",
"**/node_modules/**",
"**/dist/**",
"**/e2e/**",
"**/coverage/**",
],
},
},
},
nitro: {
watchOptions: {
ignored: [
"**/.git/**",
"**/.nuxt/**",
"**/.output/**",
"**/node_modules/**",
"**/dist/**",
"**/e2e/**",
"**/coverage/**",
],
}, },
}, },
runtimeConfig: { runtimeConfig: {

View file

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": " nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare", "postinstall": "nuxt prepare",

View file

@ -1,31 +1,32 @@
import { defineConfig } from '@playwright/test' import { defineConfig } from "@playwright/test";
export default defineConfig({ export default defineConfig({
testDir: './e2e', testDir: "./e2e",
outputDir: 'e2e/test-results', outputDir: "e2e/test-results",
snapshotDir: 'e2e/__screenshots__', snapshotDir: "e2e/__screenshots__",
fullyParallel: true, fullyParallel: true,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0, retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
reporter: 'html', reporter: "html",
use: { use: {
baseURL: 'http://localhost:3000', baseURL: "http://localhost:3000",
trace: 'on-first-retry', trace: "on-first-retry",
}, },
projects: [ projects: [
{ {
name: 'chromium', name: "chromium",
use: { browserName: 'chromium' }, use: { browserName: "chromium" },
}, },
], ],
webServer: { webServer: {
command: 'npm run dev', command: "npm run build && NODE_ENV=development npm run preview",
url: 'http://localhost:3000', url: "http://localhost:3000",
reuseExistingServer: !process.env.CI, reuseExistingServer: false,
env: { env: {
NUXT_PUBLIC_COMING_SOON: 'false', NUXT_PUBLIC_COMING_SOON: "false",
NODE_ENV: 'development', NODE_ENV: "development",
ALLOW_DEV_TEST_ENDPOINTS: "true",
}, },
}, },
}) });

View file

@ -5,109 +5,128 @@
* Safe to run multiple times. * Safe to run multiple times.
*/ */
import 'dotenv/config' import "dotenv/config";
import mongoose from 'mongoose' import mongoose from "mongoose";
import Tag from '../server/models/tag.js' import Tag from "../server/models/tag.js";
import { connectDB } from '../server/utils/mongoose.js' import { connectDB } from "../server/utils/mongoose.js";
// Convert a slug like "qa-and-testing" to "QA and Testing" // Convert a slug like "qa-and-testing" to "QA and Testing"
// Special-cases common abbreviations. // Special-cases common abbreviations.
const ABBREVIATIONS = new Map([ const ABBREVIATIONS = new Map([
['qa', 'QA'], ["qa", "QA"],
['ux', 'UX'], ["ux", "UX"],
['ui', 'UI'], ["ui", "UI"],
['devops', 'DevOps'], ["devops", "DevOps"],
]) ]);
function slugToLabel(slug) { function slugToLabel(slug) {
return slug return slug
.split('-') .split("-")
.map((word) => ABBREVIATIONS.get(word) ?? word.charAt(0).toUpperCase() + word.slice(1)) .map(
.join(' ') (word) =>
ABBREVIATIONS.get(word) ?? word.charAt(0).toUpperCase() + word.slice(1),
)
.join(" ");
} }
const CRAFT_SLUGS = [ const CRAFT_SLUGS = [
'game-design', "game-design",
'programming', "programming",
'narrative-design', "narrative-design",
'art-and-animation', "art-and-animation",
'audio-and-music', "audio-and-music",
'production-management', "production-management",
'qa-and-testing', "qa-and-testing",
'community-management', "community-management",
'marketing-and-comms', "marketing-and-comms",
'ux-and-ui-design', "ux-and-ui-design",
'business-development', "business-development",
'devops-and-tools', "devops-and-tools",
'localization', "localization",
'accessibility', "accessibility",
'analytics-and-data', "analytics-and-data",
'education-and-mentoring', "education-and-mentoring",
] ];
const COOPERATIVE_SLUGS = [ const COOPERATIVE_SLUGS = [
'governance', "governance",
'finance-and-budgeting', "finance-and-budgeting",
'legal-structures', "legal-structures",
'conflict-resolution', "conflict-resolution",
'consensus-decision-making', "consensus-decision-making",
'revenue-sharing', "revenue-sharing",
'cooperative-bylaws', "cooperative-bylaws",
'member-onboarding', "member-onboarding",
'democratic-management', "democratic-management",
'worker-ownership', "worker-ownership",
'platform-cooperativism', "platform-cooperativism",
'cooperative-marketing', "cooperative-marketing",
'shared-resources', "shared-resources",
'cooperative-funding', "cooperative-funding",
'community-building', "community-building",
'equity-and-inclusion', "equity-and-inclusion",
'cooperative-tech', "cooperative-tech",
'sustainability', "sustainability",
'collective-bargaining', "collective-bargaining",
'inter-coop-collaboration', "inter-coop-collaboration",
] ];
async function seedTags() { async function seedTags() {
await connectDB() await connectDB();
const tagDefs = [ const tagDefs = [
...CRAFT_SLUGS.map((slug) => ({ slug, pool: 'craft', label: slugToLabel(slug) })), ...CRAFT_SLUGS.map((slug) => ({
...COOPERATIVE_SLUGS.map((slug) => ({ slug, pool: 'cooperative', label: slugToLabel(slug) })), slug,
] pool: "craft",
label: slugToLabel(slug),
})),
...COOPERATIVE_SLUGS.map((slug) => ({
slug,
pool: "cooperative",
label: slugToLabel(slug),
})),
];
let upserted = 0 let upserted = 0;
let unchanged = 0 let unchanged = 0;
for (const { slug, pool, label } of tagDefs) { for (const { slug, pool, label } of tagDefs) {
const result = await Tag.updateOne( const result = await Tag.updateOne(
{ slug }, { slug },
{ $setOnInsert: { slug, pool, label, active: true, createdAt: new Date() } }, {
{ upsert: true } $setOnInsert: {
) slug,
pool,
label,
active: true,
createdAt: new Date(),
},
},
{ upsert: true },
);
if (result.upsertedCount > 0) { if (result.upsertedCount > 0) {
console.log(` + Created [${pool}] ${label} (${slug})`) console.log(` + Created [${pool}] ${label} (${slug})`);
upserted++ upserted++;
} else { } else {
unchanged++ unchanged++;
} }
} }
console.log('\n=== Seed Complete ===') console.log("\n=== Seed Complete ===");
console.log(` Total tags defined: ${tagDefs.length}`) console.log(` Total tags defined: ${tagDefs.length}`);
console.log(` Newly created: ${upserted}`) console.log(` Newly created: ${upserted}`);
console.log(` Already existed: ${unchanged}`) console.log(` Already existed: ${unchanged}`);
} }
seedTags() seedTags()
.then(() => { .then(() => {
console.log('\nTag seed completed successfully') console.log("\nTag seed completed successfully");
process.exit(0) process.exit(0);
}) })
.catch((err) => { .catch((err) => {
console.error('\nTag seed failed:', err) console.error("\nTag seed failed:", err);
process.exit(1) process.exit(1);
}) })
.finally(() => { .finally(() => {
mongoose.connection.close() mongoose.connection.close();
}) });

View file

@ -1,41 +1,50 @@
import jwt from 'jsonwebtoken' import jwt from "jsonwebtoken";
import Member from '../../models/member.js' import Member from "../../models/member.js";
import { connectDB } from '../../utils/mongoose.js' import { connectDB } from "../../utils/mongoose.js";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
// Only allow in development // Only allow in development, unless explicitly enabled for Playwright preview runs
if (process.env.NODE_ENV === 'production') { if (
throw createError({ statusCode: 404, statusMessage: 'Not found' }) process.env.NODE_ENV === "production" &&
process.env.ALLOW_DEV_TEST_ENDPOINTS !== "true"
) {
throw createError({ statusCode: 404, statusMessage: "Not found" });
} }
const query = getQuery(event) const query = getQuery(event);
const email = query.email const email = query.email;
if (!email) { if (!email) {
throw createError({ statusCode: 400, statusMessage: 'email query param required' }) throw createError({
statusCode: 400,
statusMessage: "email query param required",
});
} }
await connectDB() await connectDB();
const member = await Member.findOne({ email: email.toLowerCase() }) const member = await Member.findOne({ email: email.toLowerCase() });
if (!member) { if (!member) {
throw createError({ statusCode: 404, statusMessage: `No member found with email: ${email}` }) throw createError({
statusCode: 404,
statusMessage: `No member found with email: ${email}`,
});
} }
const config = useRuntimeConfig(event) const config = useRuntimeConfig(event);
const token = jwt.sign( const token = jwt.sign(
{ memberId: member._id, email: member.email, tv: member.tokenVersion }, { memberId: member._id, email: member.email, tv: member.tokenVersion },
config.jwtSecret, config.jwtSecret,
{ expiresIn: '7d' } { expiresIn: "7d" },
) );
setCookie(event, 'auth-token', token, { setCookie(event, "auth-token", token, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
sameSite: 'lax', sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, maxAge: 60 * 60 * 24 * 7,
}) });
await sendRedirect(event, '/member/account', 302) await sendRedirect(event, "/member/account", 302);
}) });

View file

@ -1,19 +1,24 @@
import Member from '../../models/member.js' import Member from "../../models/member.js";
import { connectDB } from '../../utils/mongoose.js' import { connectDB } from "../../utils/mongoose.js";
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
if (process.env.NODE_ENV === 'production') { if (
throw createError({ statusCode: 404, statusMessage: 'Not found' }) process.env.NODE_ENV === "production" &&
process.env.ALLOW_DEV_TEST_ENDPOINTS !== "true"
) {
throw createError({ statusCode: 404, statusMessage: "Not found" });
} }
await connectDB() await connectDB();
const members = await Member.find({}, 'name email circle role status').sort({ name: 1 }).lean() const members = await Member.find({}, "name email circle role status")
.sort({ name: 1 })
.lean();
return members.map((m) => ({ return members.map((m) => ({
label: `${m.name} (${m.email})`, label: `${m.name} (${m.email})`,
value: m.email, value: m.email,
circle: m.circle, circle: m.circle,
role: m.role role: m.role,
})) }));
}) });

View file

@ -1,42 +1,45 @@
import jwt from 'jsonwebtoken' import jwt from "jsonwebtoken";
import Member from '../../models/member.js' import Member from "../../models/member.js";
import { connectDB } from '../../utils/mongoose.js' import { connectDB } from "../../utils/mongoose.js";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
// Only allow in development // Only allow in development, unless explicitly enabled for Playwright preview runs
if (process.env.NODE_ENV === 'production') { if (
throw createError({ statusCode: 404, statusMessage: 'Not found' }) process.env.NODE_ENV === "production" &&
process.env.ALLOW_DEV_TEST_ENDPOINTS !== "true"
) {
throw createError({ statusCode: 404, statusMessage: "Not found" });
} }
await connectDB() await connectDB();
// Find or create a test admin user // Find or create a test admin user
let member = await Member.findOne({ email: 'test-admin@ghostguild.dev' }) let member = await Member.findOne({ email: "test-admin@ghostguild.dev" });
if (!member) { if (!member) {
member = await Member.create({ member = await Member.create({
email: 'test-admin@ghostguild.dev', email: "test-admin@ghostguild.dev",
name: 'Test Admin', name: "Test Admin",
circle: 'founder', circle: "founder",
contributionTier: '0', contributionTier: "0",
role: 'admin', role: "admin",
status: 'active', status: "active",
}) });
} }
const config = useRuntimeConfig(event) const config = useRuntimeConfig(event);
const token = jwt.sign( const token = jwt.sign(
{ memberId: member._id, email: member.email, tv: member.tokenVersion }, { memberId: member._id, email: member.email, tv: member.tokenVersion },
config.jwtSecret, config.jwtSecret,
{ expiresIn: '7d' } { expiresIn: "7d" },
) );
setCookie(event, 'auth-token', token, { setCookie(event, "auth-token", token, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
sameSite: 'lax', sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, maxAge: 60 * 60 * 24 * 7,
}) });
await sendRedirect(event, '/admin', 302) await sendRedirect(event, "/admin", 302);
}) });