Skip to content
Draft
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
38 changes: 27 additions & 11 deletions src/components/AuthModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getSocialLinks } from '@/lib/social-urls';

import { supabase } from '@/lib/supabase';
import { useProfile } from '@/hooks/useProfile';
import { useXVerification } from '@/hooks/useXVerification';
import { useFriends } from '@/hooks/useFriends';
import { useFriendRequests } from '@/hooks/useFriendRequests';
import type { ETHDenverEvent, NativeAd, UserSearchResult, FriendRequest } from '@/lib/types';
Expand Down Expand Up @@ -377,6 +378,7 @@ export function UserMenu({ events, itinerary, onOpenFriends, onSubmitEvent, pend
const { friendCount, refreshFriends: localRefreshFriends } = useFriends();
const { config } = useAdminConfig();
const { theme, setTheme } = useTheme();
const { isXVerified, linkX, unlinkX } = useXVerification();

const refreshFriends = useCallback(async () => {
await localRefreshFriends();
Expand Down Expand Up @@ -765,18 +767,32 @@ export function UserMenu({ events, itinerary, onOpenFriends, onSubmitEvent, pend
</div>
</div>

<div>
<div className="flex items-center bg-[var(--theme-bg-primary)] border border-[var(--theme-border-primary)] rounded-lg focus-within:border-[var(--theme-accent)]">
<span className="text-[var(--theme-text-muted)] text-sm ml-3 shrink-0 select-none font-bold leading-none" style={{ fontFamily: 'serif' }}>{'\u{1D54F}'}</span>
<input
type="text"
value={xHandle}
onChange={(e) => setXHandle(e.target.value.replace(/^@/, ''))}
placeholder="X handle"
className="flex-1 bg-transparent text-[var(--theme-text-primary)] text-sm px-2 py-2 focus:outline-none placeholder:text-[var(--theme-text-muted)]"
/>
{/* X Account */}
{profile?.x_verified ? (
<div className="flex items-center gap-2 bg-[var(--theme-bg-primary)] border border-green-500/30 rounded-lg px-3 py-2">
<svg viewBox="0 0 24 24" className="w-4 h-4 fill-current text-[var(--theme-text-secondary)]" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
<span className="text-sm text-[var(--theme-text-primary)]">@{profile.x_handle}</span>
<Check className="w-3.5 h-3.5 text-green-400" />
<button
onClick={unlinkX}
className="ml-auto text-xs text-red-400 hover:text-red-300 transition-colors cursor-pointer"
>
Disconnect
</button>
</div>
</div>
) : (
<button
onClick={linkX}
className="flex items-center gap-2 w-full bg-[var(--theme-bg-primary)] border border-[var(--theme-border-primary)] hover:border-[var(--theme-accent)] rounded-lg px-3 py-2 text-sm text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] transition-colors cursor-pointer"
>
<svg viewBox="0 0 24 24" className="w-4 h-4 fill-current" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Connect X Account
</button>
)}

<div>
<div className="flex items-center bg-[var(--theme-bg-primary)] border border-[var(--theme-border-primary)] rounded-lg focus-within:border-[var(--theme-accent)]">
Expand Down
105 changes: 99 additions & 6 deletions src/components/CommentSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { createPortal } from 'react-dom';
import { Send, Trash2, MessageCircle, X } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { useEventComments } from '@/hooks/useEventComments';
import { supabase } from '@/lib/supabase';
import { timeAgo } from '@/lib/time-parse';
import { getDisplayName } from '@/lib/user-display';
import UserAvatar from './UserAvatar';
import ProfileCardModal from './ProfileCardModal';
import { trackCommentExpand, trackCommentAdd, trackCommentDelete, trackCommentVisibilityToggle } from '@/lib/analytics';
import { useXVerification } from '@/hooks/useXVerification';

interface CommentSectionProps {
eventId: string;
Expand All @@ -19,13 +21,18 @@ interface CommentSectionProps {

export function CommentSection({ eventId, commentCount = 0, eventName }: CommentSectionProps) {
const { user } = useAuth();
const { isXVerified, linkX } = useXVerification();
const [expanded, setExpanded] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const { comments, loading, addComment, deleteComment } = useEventComments(
expanded ? eventId : null
);
const [text, setText] = useState('');
const [visibility, setVisibility] = useState<'public' | 'friends'>('public');
const [showEmailPrompt, setShowEmailPrompt] = useState(false);
const [email, setEmail] = useState('');
const [emailLoading, setEmailLoading] = useState(false);
const [emailError, setEmailError] = useState<string | null>(null);
const [selectedProfile, setSelectedProfile] = useState<{
userId: string;
displayName?: string | null;
Expand Down Expand Up @@ -163,8 +170,37 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment
</>
);

// Handle email sign-up then X OAuth link
const handleEmailThenLinkX = async () => {
if (!email.trim()) return;
setEmailLoading(true);
setEmailError(null);

// Sign up with a random password — auto-confirms since enable_confirmations = false
// Next login they'll use OTP
const { error } = await supabase.auth.signUp({
email: email.trim(),
password: crypto.randomUUID(),
});

if (error) {
// If account exists, they need to sign in properly
if (error.message?.includes('already registered') || error.status === 422) {
setEmailError('Account exists — sign in from the menu first');
} else {
setEmailError(error.message);
}
setEmailLoading(false);
return;
}

// Account created + session active, now start X OAuth
setEmailLoading(false);
linkX();
};

// Shared input content
const inputContent = user && (
const inputContent = user && isXVerified ? (
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center gap-1 bg-[var(--theme-bg-tertiary)]/50 border border-[var(--theme-border-primary)] rounded-lg px-2 py-1.5">
<input
Expand Down Expand Up @@ -202,6 +238,64 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment
<Send className="w-3.5 h-3.5" />
</button>
</div>
) : user && !isXVerified ? (
// Logged in but X not verified — go straight to X OAuth
<button
onClick={(e) => {
e.stopPropagation();
linkX();
}}
className="flex items-center justify-center gap-2 w-full py-2.5 px-3 rounded-lg border border-[var(--theme-border-primary)] bg-[var(--theme-bg-tertiary)]/50 text-sm text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] hover:border-[var(--theme-accent)] transition-colors cursor-pointer"
>
<svg viewBox="0 0 24 24" className="w-4 h-4 fill-current" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Connect X to comment
</button>
) : showEmailPrompt ? (
// Not logged in — email prompt before X OAuth
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleEmailThenLinkX();
}
}}
placeholder="Enter your email..."
className="flex-1 bg-[var(--theme-bg-tertiary)]/50 border border-[var(--theme-border-primary)] rounded-lg px-3 py-2 text-sm text-[var(--theme-text-primary)] placeholder-slate-500 outline-none min-w-0"
autoFocus
/>
<button
onClick={handleEmailThenLinkX}
disabled={!email.trim() || emailLoading}
className="px-3 py-2 rounded-lg bg-[var(--theme-accent)] hover:bg-[var(--theme-accent-hover)] disabled:bg-[var(--theme-bg-tertiary)] disabled:text-[var(--theme-text-muted)] text-[var(--theme-accent-text)] text-sm transition-colors cursor-pointer disabled:cursor-not-allowed"
>
{emailLoading ? '...' : 'Go'}
</button>
</div>
{emailError && (
<p className="text-xs text-red-400">{emailError}</p>
)}
</div>
) : (
// Not logged in — show connect CTA
<button
onClick={(e) => {
e.stopPropagation();
setShowEmailPrompt(true);
}}
className="flex items-center justify-center gap-2 w-full py-2.5 px-3 rounded-lg border border-[var(--theme-border-primary)] bg-[var(--theme-bg-tertiary)]/50 text-sm text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] hover:border-[var(--theme-accent)] transition-colors cursor-pointer"
>
<svg viewBox="0 0 24 24" className="w-4 h-4 fill-current" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Connect X to comment
</button>
);

// Mobile: render bottom-sheet modal via portal
Expand Down Expand Up @@ -257,11 +351,9 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment
</div>

{/* Sticky input at bottom */}
{user && (
<div className="border-t border-[var(--theme-border-primary)] px-4 py-3" style={{ paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))' }}>
{inputContent}
</div>
)}
<div className="border-t border-[var(--theme-border-primary)] px-4 py-3" style={{ paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))' }}>
{inputContent}
</div>
</div>
</div>,
document.body
Expand Down Expand Up @@ -321,6 +413,7 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment
telegramHandle={selectedProfile.telegramHandle}
/>
)}
<AuthModal isOpen={showAuth} onClose={() => setShowAuth(false)} />
</div>
);
}
3 changes: 1 addition & 2 deletions src/components/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,7 @@ export const EventCard = memo(function EventCard({
compact={compact}
/>
)}
{/* Comments disabled until social verification is in place */}
{/* <CommentSection eventId={event.id} commentCount={commentCount} eventName={event.name} /> */}
<CommentSection eventId={event.id} commentCount={commentCount} eventName={event.name} />
{checkedInFriends && checkedInFriends.length > 0 && (
<button
onClick={(e) => {
Expand Down
12 changes: 7 additions & 5 deletions src/hooks/useEventComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,19 @@ export function useEventComments(eventId: string | null) {

// Fetch display names for comment authors
const userIds = [...new Set((data ?? []).map((c: { user_id: string }) => c.user_id))];
let profileMap = new Map<string, { display_name: string | null; x_handle: string | null; rsvp_name: string | null; avatar_url: string | null; job_title: string | null; company: string | null; linkedin_url: string | null; telegram_handle: string | null }>();
let profileMap = new Map<string, { display_name: string | null; x_handle: string | null; rsvp_name: string | null; avatar_url: string | null; job_title: string | null; company: string | null; linkedin_url: string | null; telegram_handle: string | null; x_verified: boolean }>();

if (userIds.length > 0) {
const { data: profiles } = await supabase
.from('profiles')
.select('user_id, display_name, x_handle, rsvp_name, avatar_url, job_title, company, linkedin_url, telegram_handle')
.select('user_id, display_name, x_handle, rsvp_name, avatar_url, job_title, company, linkedin_url, telegram_handle, x_verified')
.in('user_id', userIds);

if (profiles) {
profileMap = new Map(
profiles.map((p: { user_id: string; display_name: string | null; x_handle: string | null; rsvp_name: string | null; avatar_url: string | null; job_title: string | null; company: string | null; linkedin_url: string | null; telegram_handle: string | null }) => [
profiles.map((p: { user_id: string; display_name: string | null; x_handle: string | null; rsvp_name: string | null; avatar_url: string | null; job_title: string | null; company: string | null; linkedin_url: string | null; telegram_handle: string | null; x_verified: boolean }) => [
p.user_id,
{ display_name: p.display_name, x_handle: p.x_handle, rsvp_name: p.rsvp_name, avatar_url: p.avatar_url, job_title: p.job_title, company: p.company, linkedin_url: p.linkedin_url, telegram_handle: p.telegram_handle },
{ display_name: p.display_name, x_handle: p.x_handle, rsvp_name: p.rsvp_name, avatar_url: p.avatar_url, job_title: p.job_title, company: p.company, linkedin_url: p.linkedin_url, telegram_handle: p.telegram_handle, x_verified: p.x_verified ?? false },
])
);
}
Expand All @@ -69,6 +69,7 @@ export function useEventComments(eventId: string | null) {
company: profile?.company ?? undefined,
linkedin_url: profile?.linkedin_url ?? undefined,
telegram_handle: profile?.telegram_handle ?? undefined,
x_verified: profile?.x_verified ?? false,
};
})
);
Expand Down Expand Up @@ -102,7 +103,7 @@ export function useEventComments(eventId: string | null) {
// Fetch own profile for display
const { data: profile } = await supabase
.from('profiles')
.select('display_name, x_handle, rsvp_name, avatar_url, job_title, company, linkedin_url, telegram_handle')
.select('display_name, x_handle, rsvp_name, avatar_url, job_title, company, linkedin_url, telegram_handle, x_verified')
.eq('user_id', user.id)
.single();

Expand All @@ -119,6 +120,7 @@ export function useEventComments(eventId: string | null) {
company: profile?.company ?? undefined,
linkedin_url: profile?.linkedin_url ?? undefined,
telegram_handle: profile?.telegram_handle ?? undefined,
x_verified: profile?.x_verified ?? false,
},
]);
}
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function useProfile() {
try {
const { data, error } = await supabase
.from('profiles')
.select('user_id, email, display_name, x_handle, rsvp_name, avatar_url, telegram_handle, company, linkedin_url, job_title')
.select('user_id, email, display_name, x_handle, rsvp_name, avatar_url, telegram_handle, company, linkedin_url, job_title, x_verified, x_oauth_id, x_avatar_url')
.eq('user_id', user!.id)
.maybeSingle();

Expand All @@ -116,6 +116,9 @@ export function useProfile() {
company: null,
linkedin_url: null,
job_title: null,
x_verified: false,
x_oauth_id: null,
x_avatar_url: null,
};

const { error: insertError } = await supabase
Expand All @@ -126,7 +129,7 @@ export function useProfile() {
// Could be a race condition — try fetching again
const { data: retryData } = await supabase
.from('profiles')
.select('user_id, email, display_name, x_handle, rsvp_name, avatar_url, telegram_handle, company, linkedin_url, job_title')
.select('user_id, email, display_name, x_handle, rsvp_name, avatar_url, telegram_handle, company, linkedin_url, job_title, x_verified, x_oauth_id, x_avatar_url')
.eq('user_id', user!.id)
.maybeSingle();

Expand Down
Loading