Enhance UI and functionality: Update main page layout, add contribution options in join form, and improve member dashboard display. Refactor API endpoints for member creation and login.

This commit is contained in:
Jennie Robinson Faber 2025-08-26 18:21:52 +01:00
parent 3ad127ed78
commit 6e7e27ac4e
8 changed files with 885 additions and 44 deletions

810
app/assets/css/main.css Normal file
View file

@ -0,0 +1,810 @@
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
/* Font families */
--font-sans: "Inter", "Neue Montreal", sans-serif;
--font-body: "Inter", sans-serif;
--font-mono: "Ubuntu Mono", monospace;
--font-display: "NB Television Pro", monospace;
/* Custom colors */
--color-lavender-50: #fcf6fd;
--color-lavender-100: #f7ebfc;
--color-lavender-200: #f0d7f7;
--color-lavender-300: #e5b7f0;
--color-lavender-400: #d689e5;
--color-lavender-500: #c25fd6;
--color-lavender-600: #a840b9;
--color-lavender-700: #8d3299;
--color-lavender-800: #752b7d;
--color-lavender-900: #622867;
--color-lavender-950: #3e0f43;
/* Named colors for glow effects */
--color-peachFuzz: #ffcc99;
--color-lemonChiffon: #ffff99;
--color-mintSpray: #ccff99;
--color-jadeMist: #99ff99;
--color-aquaFresh: #99ffcc;
--color-skyKiss: #99ccff;
--color-lavenderHush: #9999ff;
--color-purpleHaze: #cc99ff;
--color-pinkBliss: #ff99cc;
--color-body: #cccccc;
/* Animations */
--animate-float-1: ghostie1 3s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite alternate;
--animate-float-2: ghostie2 2.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite alternate;
--animate-float-3: ghostie3 2s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite alternate;
}
/* Keyframes for animations */
@keyframes ghostie1 {
0% { transform: translateY(0); }
100% { transform: translateY(-20px); }
}
@keyframes ghostie2 {
0% { transform: translateY(0); }
100% { transform: translateY(-15px); }
}
@keyframes ghostie3 {
0% { transform: translateY(0); }
100% { transform: translateY(-10px); }
}
.what-we-do ul {
@apply list-disc list-inside space-y-2;
}
.what-we-do a {
@apply text-lavender-400 hover:text-lavender-300 transition-colors;
}
.gradient-text {
@apply text-zinc-50 text-2xl md:text-4xl;
a& {
text-decoration: underline;
}
}
.glow-bg {
position: relative;
}
/* Force dark mode by default - ensures no flash of light content */
html {
color-scheme: dark;
}
body {
@apply font-sans antialiased text-zinc-300 bg-zinc-800;
/* background-image: url(/img/background_top_edge.png); */
background-repeat: repeat-x;
background-size: 20%;
a:not(.glow-link), a:not(.cta-button) {
@apply hover:brightness-125;
}
}
main {
/* @apply pb-0; */
}
body,
.rainbow-border {
/* border-top: 1px solid;
border-image: linear-gradient(
to right,
#ff9999,
#ffcc99,
#ffff99,
#ccff99,
#99ff99,
#99ffcc,
#99ccff,
#9999ff,
#cc99ff,
#ff99cc
);
border-image-slice: 1; */
}
.gradient-header::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
height: 1px;
width: 100%;
background: linear-gradient(
to right,
#ff9999,
#ffcc99,
#ffff99,
#ccff99,
#99ff99,
#99ffcc,
#99ccff,
#9999ff,
#cc99ff,
#ff99cc
);
}
.gradient-border {
position: relative;
}
.gradient-border::before {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
margin: -2px; /* Adjust to your desired border width */
border-radius: inherit;
background: linear-gradient(
to right,
#ff9999,
#ffcc99,
#ffff99,
#ccff99,
#99ff99,
#99ffcc,
#99ccff,
#9999ff,
#cc99ff,
#ff99cc
);
}
@define-mixin text-crt $size {
/* background-image: url("~/assets/img/CRT_screen_pattern_11x16.png");
background-repeat: repeat;
background-size: 50%;
background-clip: text;
-webkit-background-clip: text; */
/* mix-blend-mode: exclusion; */
/* color: transparent; color: rgba(245, 235, 237, 0.7); */
/* letter-spacing: -1px; */
/* Adjusting the opacity and spread of the white shadow based on size */
text-shadow: 2px 0 4px rgba(220, 148, 232, 1),
-2px 0 4px rgba(92, 201, 245, 1),
0 0 10px rgba(255, 255, 255, 0.22);
}
h1 {
.text-bg {
@apply text-purple-400;
}
font-family: "NB Television Pro", monospace;
letter-spacing: -1px;
font-smooth: auto;
-webkit-font-smoothing: auto;
}
h2 {
font-family: "NB Television Pro", monospace;
@apply text-3xl;
font-smooth: auto;
-webkit-font-smoothing: auto;
/* @mixin text-crt 0.5; */
}
h3 {
@apply uppercase text-2xl font-bold text-lavender-300 mb-0 tracking-wide;
font-family: "NB Television Pro", monospace;
@apply text-3xl mt-8;
font-smooth: auto;
-webkit-font-smoothing: auto;
@mixin text-crt 0.3;
/* letter-spacing: -1px; */
}
.font-display {
font-family: "NB Television Pro", monospace;
font-smooth: auto;
-webkit-font-smoothing: auto;
}
nav {
ul {
li {
a {
font-family: "NB Television Pro", monospace;
@apply text-2xl;
}
}
}
}
p,
li {
@apply font-normal;
strong,
b {
@apply font-bold brightness-125;
}
}
.text-prose {
@screen md {
@apply mt-5 text-xl;
}
p {
@apply mt-3 mx-auto max-w-4xl;
@screen sm {
@apply text-lg;
}
}
}
.bg-zinc-300 {
@apply text-zinc-900;
}
section {
@apply mb-16 px-6 max-w-7xl;
p a,
li a {
@apply hover:brightness-125 underline;
}
&:last-child {
/* @apply mb-0; */
}
section {
@apply px-0 mb-0;
}
&.wide {
@apply mb-0;
}
@screen md {
/* @apply mb-24 px-12; */
}
/* todo fix this situation */
@screen lg {
&:last-child {
/* @apply mb-0; */
}
}
p {
@apply mt-3;
}
p,
li {
@apply text-base;
@screen sm {
@apply text-lg;
}
}
h2 {
@apply flex-1 mb-2;
}
}
.text-arrow {
@mixin text-crt 0.3;
}
.page--news {
.article-title--excerpt {
@apply text-xl md:text-2xl font-bold text-zinc-200;
}
}
.page--about {
#staff-cards {
/* these are here because we need access to the mixin */
h2 {
@mixin text-crt 0.4;
@apply text-3xl font-bold text-zinc-300 my-0;
}
h3 {
@apply uppercase font-body text-xs text-zinc-300 mt-0 mb-2 tracking-normal;
}
/* @screen md {
h2 {
@apply text-xl my-0;
}
} */
}
}
.page--baby-ghosts {
main {
ul {
@apply list-none m-0 mt-4 p-0;
li {
@apply relative pl-6 mb-3;
&:before {
content: "";
@apply absolute left-0 top-0;
}
}
}
}
li > ul {
@apply mt-0;
li {
@apply mb-0;
}
}
li,
p {
a {
@apply text-zinc-200 hover:text-zinc-50;
}
}
div + div h3:first-child {
@apply mt-0;
}
h3 {
@apply mt-8 leading-10 mb-0;
&#coordinators {
@apply lg:mt-0 mt-12;
}
}
section {
@apply px-6;
&:not(.header):not(.wide) {
@apply text-zinc-300;
}
}
.timeline {
h4 {
@apply text-2xl leading-10 mt-12 font-bold text-zinc-100 uppercase text-primary;
}
h5 {
@apply text-base mb-6;
}
strong,
b {
@apply font-bold text-zinc-200;
}
}
.right-aligner {
h3 {
@apply mb-4 lg:mb-0 lg:text-right text-4xl;
}
ul {
@apply mt-0;
}
}
.cta {
@apply mx-auto w-full text-center pb-12;
@screen md {
@apply pb-0;
}
}
.action-buttons {
@apply flex flex-wrap justify-around my-12;
@screen lg {
@apply my-0;
}
.cta {
@apply inline w-full md:w-1/2 mb-6 md:mb-0;
&:last-child {
@apply mb-0;
}
}
}
h3:not(:first-child),
h4:not(:first-child) {
@apply mt-10;
}
}
.bg-ghostie {
&.ghostie-double-take {
background-image: url(/img/ghosts/Ghost-Double-Take.svg);
}
&.ghostie-sweet {
background-image: url(/img/ghosts/Ghost-Sweet.svg);
}
&.ghostie-happy {
background-image: url(/img/ghosts/Ghost-Happy.svg);
}
background-repeat: no-repeat;
background-position: right -10px bottom -50px;
background-size: 200px;
background-image: none;
overflow: hidden;
&.flip {
transform: scaleX(-1);
}
/* p,
li {
@apply bg-zinc-300;
} */
}
@keyframes wiggle {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(-5deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(5deg);
}
100% {
transform: rotate(0deg);
}
}
@define-mixin glow $color {
text-shadow: 0 0 10px $color, 0 0 20px $color, 0 0 30px $color,
0 0 40px $color;
}
.router-link-active {
@apply text-white;
}
.glow-link {
@apply no-underline inline-block transition-all duration-300 ease-in-out mr-4;
&.router-link-exact-active {
@apply text-zinc-50;
}
&.peachFuzz {
&:hover,
&.router-link-exact-active {
@mixin glow #ffcc99;
}
}
&.lemonChiffon {
&:hover,
&.router-link-exact-active {
@mixin glow #ffff99;
}
}
&.mintSpray {
&:hover,
&.router-link-exact-active {
@mixin glow #ccff99;
}
}
&.jadeMist {
&:hover,
&.router-link-exact-active {
@mixin glow #99ff99;
}
}
&.aquaFresh {
&:hover,
&.router-link-exact-active {
@mixin glow #99ffcc;
}
}
&.skyKiss {
&:hover,
&.router-link-exact-active {
@mixin glow #99ccff;
}
}
&.lavenderHush {
&:hover,
&.router-link-exact-active {
@mixin glow #9999ff;
}
}
&.purpleHaze {
&:hover,
&.router-link-exact-active {
@mixin glow #cc99ff;
}
}
&.pinkBliss {
&:hover,
&.router-link-exact-active {
@mixin glow #ff99cc;
}
}
}
.article-title--excerpt {
@apply text-xl font-bold font-body;
}
.article-title {
@mixin text-crt 0.3;
}
article {
h2 {
@apply text-3xl font-bold mt-8;
}
h3 {
@apply text-2xl font-bold mt-8 uppercase;
}
ul,
ol {
@apply list-none m-0 mt-4 p-0;
li {
@apply relative pl-6;
&:before {
content: "";
@apply absolute left-0 top-0;
}
}
}
}
ul.formkit-messages {
@apply list-none mt-2 ml-0 p-0 m-0;
li {
@apply relative p-0 m-0;
&:before {
content: "" !important;
position: absolute;
left: 0;
top: 0;
}
}
.formkit-message {
@apply text-sm inline-block bg-red-300 text-zinc-800 p-2 leading-tight font-bold;
}
}
.glow-bg {
@apply bg-zinc-800;
box-shadow: calc(4px) 0 calc(4px) rgba(220, 148, 232, 1),
calc(-2px) 0 calc(4px) rgba(92, 201, 245, 1),
0 0 calc(10px) rgba(255, 255, 255, calc(0.1));
}
.float-section {
h2 {
@apply text-2xl md:text-6xl font-bold text-zinc-300;
}
p {
@apply text-lg md:text-2xl lg:text-3xl text-zinc-300;
}
}
.page--home {
.what-we-do {
ul {
@apply list-none m-0 mt-4 p-0;
li {
@apply relative pl-6;
&:before {
content: "";
@apply absolute left-0 top-0;
}
}
}
li,
p {
@apply md:text-2xl;
a {
@apply text-zinc-200 hover:text-zinc-50;
}
}
}
}
.grain-wrapper {
position: fixed;
z-index: 100;
pointer-events: none;
}
.grain {
mix-blend-mode: hard-light;
height: 100vh;
width: 100vw;
background-image: url(~/assets/img/noise-256w.png);
opacity: 0.1;
animation: grain 0.4s steps(1) infinite;
}
@media (prefers-reduced-motion) {
.grain {
animation: none;
}
}
@keyframes grain {
0%,
100% {
background-position: 0% 0%;
}
20% {
background-position: 50% 50%;
}
40% {
background-position: 25% 25%;
}
60% {
background-position: 75% 75%;
}
80% {
background-position: 0% 100%;
}
}
.activities-table {
table {
@apply w-full;
}
tbody {
@apply space-y-4;
}
thead {
@apply text-left;
}
th {
@apply text-xl font-bold;
}
td {
@apply text-lg;
}
.activity {
@apply text-lg;
}
}
/* Time Commitment List Styles */
.page--peer-support .time-commitment ul,
.page--application ul.eligibility {
@apply space-y-1 text-lg text-zinc-300 mt-2;
li {
@apply pl-6 relative;
&:before {
content: "•";
@apply absolute left-0 text-lavender-500;
}
}
}
/* CTA Button Dithered Shadow Effect */
.cta-button {
position: relative;
isolation: isolate;
}
.cta-button::before {
content: "";
position: absolute;
inset: 0;
border-radius: 9999px;
z-index: -2;
background-image:
repeating-linear-gradient(
45deg,
transparent,
transparent 1px,
rgba(0, 0, 0, 0.3) 1px,
rgba(0, 0, 0, 0.3) 2px
),
repeating-linear-gradient(
-45deg,
transparent,
transparent 1px,
rgba(0, 0, 0, 0.2) 1px,
rgba(0, 0, 0, 0.2) 2px
);
transform: translate(4px, 4px);
transition: transform 0.3s ease;
}
.cta-button::after {
content: "";
position: absolute;
inset: 0;
border-radius: 9999px;
z-index: -1;
background-image: url('~/assets/img/noise-256w.png');
background-size: 100px 100px;
opacity: 0.15;
mix-blend-mode: multiply;
transform: translate(6px, 6px);
transition: transform 0.3s ease;
}
.cta-button:hover::before {
transform: translate(6px, 6px);
}
.cta-button:hover::after {
transform: translate(8px, 8px);
}
/* Color variations for dithered shadows */
.cta-peach::before {
background-image:
repeating-linear-gradient(
45deg,
transparent,
transparent 1px,
rgba(255, 153, 102, 0.3) 1px,
rgba(255, 153, 102, 0.3) 2px
),
repeating-linear-gradient(
-45deg,
transparent,
transparent 1px,
rgba(255, 204, 153, 0.2) 1px,
rgba(255, 204, 153, 0.2) 2px
);
}
.cta-blue::before {
background-image:
repeating-linear-gradient(
45deg,
transparent,
transparent 1px,
rgba(37, 99, 235, 0.3) 1px,
rgba(37, 99, 235, 0.3) 2px
),
repeating-linear-gradient(
-45deg,
transparent,
transparent 1px,
rgba(6, 182, 212, 0.2) 1px,
rgba(6, 182, 212, 0.2) 2px
);
}
.cta-green::before {
background-image:
repeating-linear-gradient(
45deg,
transparent,
transparent 1px,
rgba(34, 197, 94, 0.3) 1px,
rgba(34, 197, 94, 0.3) 2px
),
repeating-linear-gradient(
-45deg,
transparent,
transparent 1px,
rgba(52, 211, 153, 0.2) 1px,
rgba(52, 211, 153, 0.2) 2px
);
}
.quotes {
@apply p-6 xl:p-0 justify-around text-zinc-300;
p {
@apply block w-full text-xl;
@screen lg {
@apply w-1/3;
}
&:first-child {
@apply block text-2xl w-full;
@screen lg {
@apply w-3/5;
}
}
.attribution {
@apply text-base;
}
}
ul {
@apply mt-0;
}
}

View file

@ -2,29 +2,47 @@
<template> <template>
<div> <div>
<UContainer> <UContainer>
<UHero> <div class="py-24 text-center">
<template #title> <h1 class="text-4xl font-bold mb-4">
Pay what you can, take what you need, build what we dream Pay what you can, take what you need, build what we dream
</template> </h1>
<template #description> <p class="text-xl text-gray-600 dark:text-gray-400 mb-8">
Ghost Guild: A solidarity-based community for game developers Ghost Guild: A solidarity-based community for game developers
exploring cooperative models exploring cooperative models
</template> </p>
<template #actions> <UButton to="/join" size="lg" color="primary">
<UButton to="/join" size="lg" color="purple"> Join Ghost Guild
Join Ghost Guild </UButton>
</UButton> </div>
</template>
</UHero>
<UGrid :cols="3" class="mt-16 gap-8"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mt-16">
<UCard v-for="circle in circles" :key="circle.id"> <UCard v-for="circle in circles" :key="circle.id">
<template #header> <template #header>
<h3>{{ circle.name }}</h3> <h3>{{ circle.name }}</h3>
</template> </template>
{{ circle.description }} {{ circle.description }}
</UCard> </UCard>
</UGrid> </div>
</UContainer> </UContainer>
</div> </div>
</template> </template>
<script setup>
const circles = [
{
id: 1,
name: 'Community Circle',
description: 'Join our community for $15/month. Perfect for indie developers and students looking to connect with like-minded creators.'
},
{
id: 2,
name: 'Support Circle',
description: 'Support the community at $25/month. Get access to additional resources and help sustain solidarity memberships.'
},
{
id: 3,
name: 'Sustaining Circle',
description: 'Champion our mission at $50/month. Your contribution helps us provide more solidarity spots and expand our programs.'
}
];
</script>

View file

@ -3,9 +3,9 @@
<UContainer class="py-12"> <UContainer class="py-12">
<UForm :state="form" @submit="handleSubmit"> <UForm :state="form" @submit="handleSubmit">
<!-- Step 1: Basic Info --> <!-- Step 1: Basic Info -->
<UFormGroup label="Email" name="email"> <UFormField label="Email" name="email">
<UInput v-model="form.email" type="email" /> <UInput v-model="form.email" type="email" />
</UFormGroup> </UFormField>
<!-- Step 2: Choose Circle --> <!-- Step 2: Choose Circle -->
<URadioGroup <URadioGroup
@ -28,6 +28,8 @@
</template> </template>
<script setup> <script setup>
import { reactive, onMounted } from 'vue';
const form = reactive({ const form = reactive({
email: "", email: "",
name: "", name: "",
@ -35,6 +37,23 @@ const form = reactive({
contribution: "15", contribution: "15",
}); });
const circleOptions = [
{ value: 'community', label: 'Community Circle - $15/month' },
{ value: 'support', label: 'Support Circle - $25/month' },
{ value: 'sustaining', label: 'Sustaining Circle - $50/month' }
];
const contributionOptions = [
{ value: '15', label: '$15/month' },
{ value: '25', label: '$25/month' },
{ value: '50', label: '$50/month' },
{ value: 'custom', label: 'Custom amount' }
];
const handleSubmit = () => {
console.log('Form submitted:', form);
};
// Load Helcim.js // Load Helcim.js
onMounted(() => { onMounted(() => {
const script = document.createElement("script"); const script = document.createElement("script");

View file

@ -6,7 +6,7 @@
<template #title> Welcome back, {{ member?.name }}! </template> <template #title> Welcome back, {{ member?.name }}! </template>
</UDashboardHeader> </UDashboardHeader>
<UGrid :cols="2" class="gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<UCard> <UCard>
<template #header>Your Circle</template> <template #header>Your Circle</template>
<p class="text-xl font-semibold">{{ member?.circle }}</p> <p class="text-xl font-semibold">{{ member?.circle }}</p>
@ -25,7 +25,7 @@
Adjust Contribution Adjust Contribution
</UButton> </UButton>
</UCard> </UCard>
</UGrid> </div>
<UCard class="mt-6"> <UCard class="mt-6">
<template #header>Quick Links</template> <template #header>Quick Links</template>

View file

@ -6,4 +6,5 @@ export default defineNuxtConfig({
plausible: { plausible: {
domain: "ghostguild.org", domain: "ghostguild.org",
}, },
}); css: ["~/assets/css/main.css"],
});

View file

@ -1,6 +1,6 @@
// server/api/auth/login.post.js // server/api/auth/login.post.js
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import Member from '~/server/models/member' import Member from '../../models/member'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { email } = await readBody(event) const { email } = await readBody(event)

View file

@ -1,25 +1,17 @@
// server/api/members/create.post.js
import Member from '../../models/member'
// server/models/member.js export default defineEventHandler(async (event) => {
import mongoose from 'mongoose' const body = await readBody(event)
const memberSchema = new mongoose.Schema({ try {
email: { type: String, required: true, unique: true }, const member = new Member(body)
name: { type: String, required: true }, await member.save()
circle: { return { success: true, member }
type: String, } catch (error) {
enum: ['community', 'founder', 'practitioner'], throw createError({
required: true statusCode: 400,
}, statusMessage: error.message
contributionTier: { })
type: String, }
enum: ['0', '5', '15', '30', '50'], })
required: true
},
helcimCustomerId: String,
helcimSubscriptionId: String,
slackInvited: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
lastLogin: Date
})
export default mongoose.model('Member', memberSchema)

View file

@ -21,4 +21,5 @@ const memberSchema = new mongoose.Schema({
lastLogin: Date lastLogin: Date
}) })
export default mongoose.model('Member', memberSchema) // Check if model already exists to prevent re-compilation in development
export default mongoose.models.Member || mongoose.model('Member', memberSchema)