Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/api/auth/google/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
parseOAuthState,
} from '@/lib/google-auth'
import { createGoogleCalendarService, GoogleCalendarTokens } from '@/lib/google-calendar'
import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses'

/**
* API Route: /api/auth/google/callback
Expand Down Expand Up @@ -223,7 +224,7 @@ async function ensureUserRecord(userId: string) {
.eq('id', userId)
.single()

const userEmail = authUser?.email || `user-${userId}@localloop.app`
const userEmail = authUser?.email || EMAIL_ADDRESSES.generateUserEmail(userId)
const userName = authUser?.raw_user_meta_data?.full_name || authUser?.raw_user_meta_data?.name || 'User'

console.log(`[DEBUG] Retrieved user data: email=${userEmail}, name=${userName}`)
Expand Down
3 changes: 2 additions & 1 deletion app/api/events/cancellation/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { sendEventCancellationEmail } from '@/lib/email-service'
import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses'
import { z } from 'zod'

// Event cancellation request schema
Expand Down Expand Up @@ -207,7 +208,7 @@ export async function POST(request: NextRequest) {
eventLocation: event.location,
eventAddress: event.location_details || event.location,
organizerName: organizerData?.display_name || 'Event Organizer',
organizerEmail: organizerData?.email || 'organizer@localloop.app',
organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER,
cancellationReason: cancellation_reason,
refundAmount: 0, // Assuming refundAmount is not provided in the attendees
refundTimeframe: refund_timeframe,
Expand Down
3 changes: 2 additions & 1 deletion app/api/events/reminders/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { sendEventReminderEmail } from '@/lib/email-service'
import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses'
import { z } from 'zod'

// Event reminder request schema
Expand Down Expand Up @@ -241,7 +242,7 @@ export async function POST(request: NextRequest) {
eventLocation: event.location,
eventAddress: event.location_details || event.location,
organizerName: organizerData?.display_name || 'Event Organizer',
organizerEmail: organizerData?.email || 'organizer@localloop.app',
organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER,
rsvpId: attendee.rsvp_id,
isTicketHolder: attendee.type === 'ticket',
ticketCount: attendee.ticket_count,
Expand Down
7 changes: 4 additions & 3 deletions app/api/rsvps/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { sendRSVPCancellationEmail } from '@/lib/email-service'
import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses'
import { z } from 'zod'

// RSVP update schema
Expand Down Expand Up @@ -173,8 +174,8 @@ export async function PATCH(
: existingRsvp.guest_name || 'Guest';

const userEmail = existingRsvp.user_id
? user?.email || 'unknown@email.com'
: existingRsvp.guest_email || 'unknown@email.com';
? user?.email || EMAIL_ADDRESSES.SYSTEM_FROM
: existingRsvp.guest_email || EMAIL_ADDRESSES.SYSTEM_FROM;

// Handle organizer data from Supabase join
const organizer = Array.isArray(eventDetails.users) ? eventDetails.users[0] : eventDetails.users;
Expand Down Expand Up @@ -205,7 +206,7 @@ export async function PATCH(
eventLocation: eventDetails.location,
eventAddress: eventDetails.address || eventDetails.location,
organizerName: organizerData?.full_name || 'Event Organizer',
organizerEmail: organizerData?.email || 'organizer@localloop.app',
organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER,
rsvpId: updatedRsvp.id,
cancellationReason: updateData.notes || undefined,
eventSlug: eventDetails.slug
Expand Down
3 changes: 2 additions & 1 deletion app/api/rsvps/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase-server'
import { sendRSVPConfirmationEmail } from '@/lib/email-service'
import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses'
import { z } from 'zod'

// Performance optimization: Simple in-memory cache for RSVP checks (5 minutes)
Expand Down Expand Up @@ -382,7 +383,7 @@ export async function POST(request: NextRequest) {
eventLocation: event.location,
eventAddress: event.location_details || event.location,
organizerName: organizerData?.display_name || 'Event Organizer',
organizerEmail: organizerData?.email || 'organizer@localloop.app',
organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER,
rsvpId: newRsvp.id,
guestCount: 1,
isAuthenticated: !!user,
Expand Down
3 changes: 2 additions & 1 deletion app/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Footer } from '@/components/ui/Footer';
import { Mail, Phone, MessageCircle } from 'lucide-react';
import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses';

export default function ContactPage() {
return (
Expand All @@ -26,7 +27,7 @@ export default function ContactPage() {
</div>
<div>
<h3 className="font-semibold text-foreground">Email</h3>
<p className="text-muted-foreground">hello@localloop.events</p>
<p className="text-muted-foreground">{EMAIL_ADDRESSES.CONTACT}</p>
</div>
</div>
<div className="flex items-center gap-4">
Expand Down
28 changes: 28 additions & 0 deletions lib/config/email-addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Email address configuration - single source of truth for all LocalLoop emails
// Update the domain here to change all email addresses across the application

const EMAIL_DOMAIN = 'localloopevents.xyz';

export const EMAIL_ADDRESSES = {
// System emails (automated messages)
SYSTEM_FROM: `noreply@${EMAIL_DOMAIN}`,

// Contact and support emails
CONTACT: `hello@${EMAIL_DOMAIN}`,
SUPPORT: `support@${EMAIL_DOMAIN}`,

// Organizational emails
ORGANIZER: `organizer@${EMAIL_DOMAIN}`,

// Utility function to generate user emails
generateUserEmail: (userId: string) => `user-${userId}@${EMAIL_DOMAIN}`,
} as const;

// Legacy aliases for backwards compatibility (if needed)
export const LEGACY_EMAIL_ADDRESSES = {
HELLO: EMAIL_ADDRESSES.CONTACT,
NOREPLY: EMAIL_ADDRESSES.SYSTEM_FROM,
} as const;

// Export domain for other configurations
export const EMAIL_DOMAIN_CONFIG = EMAIL_DOMAIN;
34 changes: 21 additions & 13 deletions lib/email-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WelcomeEmail from './emails/welcome-email';
import EventReminderEmail from './emails/event-reminder';
import EventCancellationEmail from './emails/event-cancellation';
import RefundConfirmationEmail from './emails/templates/RefundConfirmationEmail';
import { EMAIL_ADDRESSES } from './config/email-addresses';

// Lazy-initialize Resend to prevent build-time failures
let resendInstance: Resend | null = null;
Expand All @@ -20,16 +21,23 @@ function getResendInstance(): Resend {
return resendInstance;
}

// ✨ DEVELOPMENT MODE: Override email for testing with Resend free tier
const isDevelopment = process.env.NODE_ENV === 'development';
// ✨ EMAIL OVERRIDE CONFIGURATION
// Use dedicated environment variable for email override control
const shouldOverrideEmails = process.env.OVERRIDE_EMAILS_TO_DEV === 'true';
const isLocalDevelopment = process.env.NODE_ENV === 'development' && process.env.VERCEL_ENV !== 'production';
const devOverrideEmail = 'jackson_rhoden@outlook.com'; // Your verified email

// Helper function to get the actual recipient email
function getRecipientEmail(originalEmail: string): string {
if (isDevelopment && originalEmail !== devOverrideEmail) {
// Only override if explicitly enabled AND we're in local development
const shouldRedirect = shouldOverrideEmails && isLocalDevelopment;

if (shouldRedirect && originalEmail !== devOverrideEmail) {
console.log(`πŸ”§ DEV MODE: Redirecting email from ${originalEmail} to ${devOverrideEmail}`);
return devOverrideEmail;
}

// Always use original email in production or when override is disabled
return originalEmail;
}

Expand Down Expand Up @@ -167,7 +175,7 @@ export async function sendRSVPConfirmationEmail(

// Send the email
const response = await getResendInstance().emails.send({
from: process.env.RESEND_FROM_EMAIL || 'LocalLoop <noreply@localloop.app>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: getRecipientEmail(props.to),
subject: `RSVP Confirmed: ${props.eventTitle}`,
html: emailHtml,
Expand Down Expand Up @@ -273,7 +281,7 @@ export async function sendWelcomeEmail(

// Send the email
const response = await getResendInstance().emails.send({
from: process.env.RESEND_FROM_EMAIL || 'LocalLoop <noreply@localloop.app>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: getRecipientEmail(props.to),
subject: 'Welcome to LocalLoop! πŸŽ‰',
html: emailHtml,
Expand Down Expand Up @@ -341,7 +349,7 @@ QUICK ACTIONS:
NEED HELP GETTING STARTED?
Visit your My Events page to manage your RSVPs and created events: ${baseUrl}/my-events

Have questions? Contact us at support@localloop.app
Have questions? Contact us at ${EMAIL_ADDRESSES.SUPPORT}

---
This email was sent by LocalLoop. You're receiving this because you created an account.
Expand Down Expand Up @@ -380,7 +388,7 @@ export async function sendEventReminderEmail(

// Send the email
const response = await getResendInstance().emails.send({
from: process.env.RESEND_FROM_EMAIL || 'LocalLoop <noreply@localloop.app>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: getRecipientEmail(props.to),
subject: getReminderSubject(),
html: emailHtml,
Expand Down Expand Up @@ -505,7 +513,7 @@ export async function sendEventCancellationEmail(

// Send the email
const response = await getResendInstance().emails.send({
from: process.env.RESEND_FROM_EMAIL || 'LocalLoop <noreply@localloop.app>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: getRecipientEmail(props.to),
subject: `Event Cancelled: ${props.eventTitle}`,
html: emailHtml,
Expand Down Expand Up @@ -589,7 +597,7 @@ QUICK ACTIONS:
β€’ Contact Organizer: ${props.organizerEmail}

Questions about this cancellation? Contact the event organizer: ${props.organizerName} at ${props.organizerEmail}
${props.isTicketHolder ? `For refund inquiries, please contact: support@localloop.app\n` : ''}
${props.isTicketHolder ? `For refund inquiries, please contact: ${EMAIL_ADDRESSES.SUPPORT}\n` : ''}
---
This cancellation notice was sent by LocalLoop on behalf of ${props.organizerName}.
Unsubscribe: ${baseUrl}/unsubscribe?email=${encodeURIComponent(props.userEmail)}
Expand Down Expand Up @@ -617,7 +625,7 @@ export async function sendRSVPCancellationEmail(

// Send the email
const response = await getResendInstance().emails.send({
from: process.env.RESEND_FROM_EMAIL || 'LocalLoop <noreply@localloop.app>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: getRecipientEmail(props.to),
subject: `RSVP Cancelled: ${props.eventTitle}`,
html: emailHtml,
Expand Down Expand Up @@ -741,7 +749,7 @@ export async function sendRefundConfirmationEmail(

// Send the email
const response = await getResendInstance().emails.send({
from: process.env.RESEND_FROM_EMAIL || 'LocalLoop <noreply@localloop.app>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: getRecipientEmail(props.to),
subject: subject,
html: emailHtml,
Expand All @@ -755,7 +763,7 @@ export async function sendRefundConfirmationEmail(
{ name: 'order', value: props.orderId }
],
// Add reply-to support email for refund inquiries
replyTo: 'support@localloop.app',
replyTo: EMAIL_ADDRESSES.SUPPORT,
});

console.log('Refund confirmation email sent successfully:', {
Expand Down Expand Up @@ -829,7 +837,7 @@ REFUND INFORMATION:
QUICK ACTIONS:
β€’ View Event Details: ${eventUrl}
β€’ Browse Other Events: ${baseUrl}/events
β€’ Contact Support: support@localloop.app
β€’ Contact Support: ${EMAIL_ADDRESSES.SUPPORT}

${isEventCancellation
? 'We apologize for any inconvenience caused by the event cancellation.'
Expand Down
2 changes: 1 addition & 1 deletion lib/emails/event-cancellation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export const EventCancellationEmail = ({

{isTicketHolder && (
<Text style={footerText}>
For refund inquiries, please contact: <Link href="mailto:support@localloop.app" style={link}>support@localloop.app</Link>
For refund inquiries, please contact: <Link href="mailto:support@localloopevents.xyz" style={link}>support@localloopevents.xyz</Link>
</Text>
)}

Expand Down
16 changes: 12 additions & 4 deletions lib/emails/send-ticket-confirmation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Resend } from 'resend';
import { TicketConfirmationEmail } from './templates/TicketConfirmationEmail';
import { EMAIL_ADDRESSES } from '../config/email-addresses';

// Lazy-initialize Resend to prevent build-time failures
let resendInstance: Resend | null = null;
Expand All @@ -14,16 +15,23 @@ function getResendInstance(): Resend {
return resendInstance;
}

// ✨ DEVELOPMENT MODE: Override email for testing with Resend free tier
const isDevelopment = process.env.NODE_ENV === 'development';
// ✨ EMAIL OVERRIDE CONFIGURATION
// Use dedicated environment variable for email override control
const shouldOverrideEmails = process.env.OVERRIDE_EMAILS_TO_DEV === 'true';
const isLocalDevelopment = process.env.NODE_ENV === 'development' && process.env.VERCEL_ENV !== 'production';
const devOverrideEmail = 'jackson_rhoden@outlook.com'; // Your verified email

// Helper function to get the actual recipient email
function getRecipientEmail(originalEmail: string): string {
if (isDevelopment && originalEmail !== devOverrideEmail) {
// Only override if explicitly enabled AND we're in local development
const shouldRedirect = shouldOverrideEmails && isLocalDevelopment;

if (shouldRedirect && originalEmail !== devOverrideEmail) {
console.log(`πŸ”§ DEV MODE: Redirecting email from ${originalEmail} to ${devOverrideEmail}`);
return devOverrideEmail;
}

// Always use original email in production or when override is disabled
return originalEmail;
}

Expand Down Expand Up @@ -61,7 +69,7 @@ export async function sendTicketConfirmationEmail({
try {
const resend = getResendInstance();
const { data, error } = await resend.emails.send({
from: 'LocalLoop Events <onboarding@resend.dev>',
from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`,
to: [getRecipientEmail(to)],
subject: `Ticket Confirmation - ${eventTitle}`,
react: TicketConfirmationEmail({
Expand Down
3 changes: 2 additions & 1 deletion lib/emails/welcome-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Heading,
} from '@react-email/components';
import * as React from 'react';
import { EMAIL_ADDRESSES } from '../config/email-addresses';

interface WelcomeEmailProps {
userName: string;
Expand Down Expand Up @@ -122,7 +123,7 @@ export const WelcomeEmail = ({
</Text>

<Text style={footerText}>
Have questions? Contact us at <Link href="mailto:support@localloop.app" style={link}>support@localloop.app</Link>
Have questions? Contact us at <Link href={`mailto:${EMAIL_ADDRESSES.SUPPORT}`} style={link}>{EMAIL_ADDRESSES.SUPPORT}</Link>
</Text>

<Hr style={divider} />
Expand Down
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
// Force consistent port and set dynamic environment
env: {
NEXT_PUBLIC_APP_URL: process.env.NODE_ENV === 'production'
? 'https://local-loop-qa.vercel.app'
? 'https://localloopevents.xyz'
: 'http://localhost:3000',
},

Expand All @@ -115,8 +115,8 @@
config.resolve = config.resolve || {}
config.resolve.fallback = {
...config.resolve.fallback,
'leaflet': false as any,

Check warning on line 118 in next.config.ts

View workflow job for this annotation

GitHub Actions / ⚑ Quick Quality Check

Unexpected any. Specify a different type
'react-leaflet': false as any,

Check warning on line 119 in next.config.ts

View workflow job for this annotation

GitHub Actions / ⚑ Quick Quality Check

Unexpected any. Specify a different type
'web-vitals': false,
'@vercel/analytics': false,
'@stripe/stripe-js': false,
Expand Down
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^16.5.0",
"google-auth-library": "^9.15.1",
"googleapis": "^149.0.0",
"lucide-react": "^0.511.0",
Expand All @@ -91,6 +90,7 @@
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"dotenv": "^16.5.0",
"eslint": "^9.16.0",
"eslint-config-next": "15.3.2",
"husky": "^9.1.7",
Expand Down
4 changes: 2 additions & 2 deletions tests/load/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ Set the base URL for different environments:
BASE_URL=http://localhost:3000 npm run load-test

# Staging environment
BASE_URL=https://staging.localloop.app npm run load-test
BASE_URL=https://staging.localloopevents.xyz npm run load-test

# Production (use with extreme caution!)
BASE_URL=https://localloop.app npm run load-test
BASE_URL=https://localloopevents.xyz npm run load-test
```

## πŸ“ˆ Understanding Results
Expand Down
Loading