mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 06:05:28 +00:00
feat(tmp): invite link feature
This commit is contained in:
@@ -16,7 +16,7 @@ import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
||||
|
||||
// --- Enums (matching the SQL CHECK constraints)
|
||||
export const eventTypeEnum = pgEnum('event_type', ['limited', 'unlimited']);
|
||||
export const visibilityEnum = pgEnum('visibility', ['public', 'private']);
|
||||
export const visibilityEnum = pgEnum('visibility', ['public', 'private', 'invite-only']);
|
||||
export const locationTypeEnum = pgEnum('location_type', ['none', 'text', 'maps']);
|
||||
|
||||
// --- Events table
|
||||
@@ -71,11 +71,31 @@ export const rsvps = pgTable(
|
||||
})
|
||||
);
|
||||
|
||||
// --- Invite Tokens table
|
||||
export const inviteTokens = pgTable(
|
||||
'invite_tokens',
|
||||
{
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
eventId: varchar('event_id', { length: 8 })
|
||||
.notNull()
|
||||
.references(() => events.id, { onDelete: 'cascade' }),
|
||||
token: varchar('token', { length: 32 }).notNull().unique(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow()
|
||||
},
|
||||
(t) => ({
|
||||
idxInviteTokensEventId: index('idx_invite_tokens_event_id').on(t.eventId),
|
||||
idxInviteTokensToken: index('idx_invite_tokens_token').on(t.token),
|
||||
idxInviteTokensExpiresAt: index('idx_invite_tokens_expires_at').on(t.expiresAt)
|
||||
})
|
||||
);
|
||||
|
||||
// --- Relations (optional but handy for type safety)
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
export const eventsRelations = relations(events, ({ many }) => ({
|
||||
rsvps: many(rsvps)
|
||||
rsvps: many(rsvps),
|
||||
inviteTokens: many(inviteTokens)
|
||||
}));
|
||||
|
||||
export const rsvpsRelations = relations(rsvps, ({ one }) => ({
|
||||
@@ -85,16 +105,30 @@ export const rsvpsRelations = relations(rsvps, ({ one }) => ({
|
||||
})
|
||||
}));
|
||||
|
||||
export const inviteTokensRelations = relations(inviteTokens, ({ one }) => ({
|
||||
event: one(events, {
|
||||
fields: [inviteTokens.eventId],
|
||||
references: [events.id]
|
||||
})
|
||||
}));
|
||||
|
||||
// --- Inferred types for use in the application
|
||||
export type Event = InferSelectModel<typeof events>;
|
||||
export type NewEvent = InferInsertModel<typeof events>;
|
||||
export type Rsvp = InferSelectModel<typeof rsvps>;
|
||||
export type NewRsvp = InferInsertModel<typeof rsvps>;
|
||||
export type InviteToken = InferSelectModel<typeof inviteTokens>;
|
||||
export type NewInviteToken = InferInsertModel<typeof inviteTokens>;
|
||||
|
||||
// --- Additional utility types
|
||||
export type EventWithRsvps = Event & {
|
||||
rsvps: Rsvp[];
|
||||
};
|
||||
|
||||
export type EventWithInviteTokens = Event & {
|
||||
inviteTokens: InviteToken[];
|
||||
};
|
||||
|
||||
export type CreateEventData = Omit<NewEvent, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
export type CreateRsvpData = Omit<NewRsvp, 'id' | 'createdAt'>;
|
||||
export type CreateInviteTokenData = Omit<NewInviteToken, 'id' | 'createdAt'>;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"visibility": "Visibility",
|
||||
"public": "Public",
|
||||
"private": "Private",
|
||||
"inviteOnly": "Invite Only",
|
||||
"limited": "Limited",
|
||||
"unlimited": "Unlimited",
|
||||
"capacity": "Capacity",
|
||||
@@ -41,7 +42,7 @@
|
||||
"numberOfGuests": "Number of Guests",
|
||||
"addGuests": "Add guest users",
|
||||
"joinEvent": "Join Event",
|
||||
"copyLink": "Copy Link",
|
||||
"copyLink": "Event link copied to clipboard.",
|
||||
"addToCalendar": "Add to Calendar",
|
||||
"close": "Close",
|
||||
"closeModal": "Close modal",
|
||||
@@ -64,9 +65,11 @@
|
||||
"eventNotFound": "Event Not Found",
|
||||
"eventIsFull": "Event is Full!",
|
||||
"maximumCapacityReached": "Maximum capacity reached",
|
||||
"eventLinkCopied": "Event link copied to clipboard!",
|
||||
"rsvpAddedSuccessfully": "RSVP added successfully!",
|
||||
"removedRsvpSuccessfully": "Removed RSVP successfully.",
|
||||
"inviteRequiredToDetails": "This event requires an invite link to see the details.",
|
||||
"invalidInviteToken": "Invalid invite token",
|
||||
"inviteTokenExpired": "Invite token has expired",
|
||||
"anUnexpectedErrorOccurred": "An unexpected error occurred.",
|
||||
"somethingWentWrong": "Something went wrong. Please try again.",
|
||||
"failedToAddRsvp": "Failed to add RSVP",
|
||||
@@ -160,8 +163,10 @@
|
||||
"visibilityLabel": "Visibility",
|
||||
"publicOption": "🌍 Public",
|
||||
"privateOption": "🔒 Private",
|
||||
"inviteOnlyOption": "🚧 Invite Only",
|
||||
"publicDescription": "Public events are visible to everyone and can be discovered by others.",
|
||||
"privateDescription": "Private events are only visible to you and people you share the link with.",
|
||||
"inviteOnlyDescription": "Event is public but requires a special invite link to attend.",
|
||||
"creatingEvent": "Creating Event...",
|
||||
"createEventButton": "Create Event"
|
||||
},
|
||||
@@ -188,9 +193,14 @@
|
||||
"noAttendeesYet": "No attendees yet",
|
||||
"beFirstToJoin": "Be the first to join!",
|
||||
"copyLinkButton": "Copy Link",
|
||||
"copyInviteLinkButton": "Copy Invite Link",
|
||||
"inviteOnlyBadge": "Invite Only",
|
||||
"inviteOnlyBannerTitle": "Invite Only Event",
|
||||
"inviteOnlyBannerSubtitle": "You're viewing this event through a special invite link",
|
||||
"addToCalendarButton": "Add to Calendar",
|
||||
"eventLinkCopied": "Event link copied to clipboard!",
|
||||
"rsvpAddedSuccessfully": "RSVP added successfully!",
|
||||
"eventLinkCopied": "Event link copied to clipboard.",
|
||||
"inviteLinkCopied": "Invite link copied to clipboard.",
|
||||
"rsvpAddedSuccessfully": "RSVP added successfully.",
|
||||
"removedRsvpSuccessfully": "Removed RSVP successfully.",
|
||||
"failedToAddRsvp": "Failed to add RSVP",
|
||||
"failedToRemoveRsvp": "Failed to remove RSVP",
|
||||
@@ -208,7 +218,8 @@
|
||||
"viewEventAriaLabel": "View event",
|
||||
"editEventAriaLabel": "Edit event",
|
||||
"deleteEventAriaLabel": "Delete event",
|
||||
"removeRsvpAriaLabel": "Remove RSVP"
|
||||
"removeRsvpAriaLabel": "Remove RSVP",
|
||||
"inviteLinkExpiresAt": "This link expires when the event starts: {time}"
|
||||
},
|
||||
"discover": {
|
||||
"title": "Discover Events - Cactoide",
|
||||
|
||||
33
src/lib/inviteTokenHelpers.ts
Normal file
33
src/lib/inviteTokenHelpers.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
/**
|
||||
* Generates a secure random token for invite links
|
||||
* @param length - Length of the token (default: 32)
|
||||
* @returns A random hex string
|
||||
*/
|
||||
export function generateInviteToken(length: number = 32): string {
|
||||
return randomBytes(length / 2).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the expiration time for an invite token
|
||||
* The token expires when the event starts
|
||||
* @param eventDate - The event date in YYYY-MM-DD format
|
||||
* @param eventTime - The event time in HH:MM:SS format
|
||||
* @returns ISO string of the expiration time
|
||||
*/
|
||||
export function calculateTokenExpiration(eventDate: string, eventTime: string): string {
|
||||
const eventDateTime = new Date(`${eventDate}T${eventTime}`);
|
||||
return eventDateTime.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an invite token is still valid
|
||||
* @param expiresAt - The expiration time as ISO string
|
||||
* @returns true if token is still valid, false otherwise
|
||||
*/
|
||||
export function isTokenValid(expiresAt: string): boolean {
|
||||
const now = new Date();
|
||||
const expiration = new Date(expiresAt);
|
||||
return now < expiration;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export type EventType = 'limited' | 'unlimited';
|
||||
export type EventVisibility = 'public' | 'private';
|
||||
export type EventVisibility = 'public' | 'private' | 'invite-only';
|
||||
export type ActionType = 'add' | 'remove';
|
||||
export type LocationType = 'none' | 'text' | 'maps';
|
||||
|
||||
@@ -62,3 +62,11 @@ export interface DatabaseRSVP {
|
||||
user_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface InviteToken {
|
||||
id: string;
|
||||
event_id: string;
|
||||
token: string;
|
||||
expires_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user