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
37 changes: 21 additions & 16 deletions Client/src/components/MentorProgramPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FormEvent, useEffect, useMemo, useState } from 'react';
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { api, ApiError } from '@/lib/api-client';
import { Calendar, Copy, ExternalLink, Loader2, ShieldAlert, Star, Video, Users, Target, Zap, ChevronRight, Search, Check, ShieldCheck, Clock, MessageSquare, Send } from 'lucide-react';
import { Calendar, Copy, ExternalLink, Loader2, ShieldAlert, Star, Video, Users, Target, Zap, ChevronRight, Search, Check, ShieldCheck, Clock, MessageSquare, Send, type LucideIcon } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';

type ExperienceLevel = 'junior' | 'mid' | 'senior' | 'expert';
type RequestStatus = 'pending' | 'accepted' | 'declined' | 'canceled' | 'completed';
type MentorRequestAction = 'accept' | 'decline' | 'confirm_complete' | 'cancel' | 'complete';

interface Mentor {
id: string;
Expand Down Expand Up @@ -116,11 +117,7 @@ const MentorProgramPanel = () => {
return `${JITSI_BASE_URL}/${MENTORSHIP_ROOM_PREFIX}-${roomSeed}`;
};

useEffect(() => {
void loadData();
}, []);

const loadData = async () => {
const loadData = useCallback(async () => {
try {
setLoading(true);
const [mentorList, mentorStats] = await Promise.all([
Expand All @@ -129,25 +126,33 @@ const MentorProgramPanel = () => {
]);
setMentors(mentorList);
setStats(mentorStats);
if (mentorList.length > 0 && !selectedMentorId) {
setSelectedMentorId(mentorList[0].id);
if (mentorList.length > 0) {
setSelectedMentorId((current) => current || mentorList[0].id);
}

try {
const myRequests = await api.get<MentorRequest[]>('/community/mentors/requests');
setRequests(myRequests);
} catch (err) {}
} catch (err) {
void err;
}

try {
const me = await api.get<Mentor>('/community/mentors/me');
setMyProfile(me);
} catch (err) {}
} catch (err) {
void err;
}
} catch (error) {
setMessage('Failed to load mission data.');
} finally {
setLoading(false);
}
};
}, []);

useEffect(() => {
void loadData();
}, [loadData]);

const handleApplyAsMentor = async (e: FormEvent) => {
e.preventDefault();
Expand All @@ -171,7 +176,7 @@ const MentorProgramPanel = () => {
};

const handleApplyFilters = async () => {
const query: any = {};
const query: Record<string, string> = {};
if (filters.skill.trim()) query.skill = filters.skill.trim();
if (filters.experienceLevel) query.experienceLevel = filters.experienceLevel;
const list = await api.get<Mentor[]>('/community/mentors', query);
Expand Down Expand Up @@ -225,7 +230,7 @@ const MentorProgramPanel = () => {
} catch (err) { setMessage('Feedback loop failed.'); }
};

const handleRequestAction = async (requestId: string, action: any) => {
const handleRequestAction = async (requestId: string, action: MentorRequestAction) => {
try {
await api.patch(`/community/mentors/requests/${requestId}`, { action });
loadData();
Expand Down Expand Up @@ -276,7 +281,7 @@ const MentorProgramPanel = () => {
<div className="flex gap-4 border-b border-border">
{['directory', 'requests'].map((tab) => (
<button
key={tab} onClick={() => setActiveTab(tab as any)}
key={tab} onClick={() => setActiveTab(tab as 'directory' | 'requests')}
className={`px-6 py-4 text-xs font-black uppercase tracking-[0.2em] transition-all relative ${activeTab === tab ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}`}
>
{tab === 'directory' ? 'Operational Dossiers' : 'Mission Logs'}
Expand Down Expand Up @@ -536,7 +541,7 @@ const MentorProgramPanel = () => {
);
};

const MetricCard = ({ icon: Icon, label, value, color }: { icon: any, label: string; value: number; color: 'red' | 'blue' | 'yellow' }) => {
const MetricCard = ({ icon: Icon, label, value, color }: { icon: LucideIcon, label: string; value: number; color: 'red' | 'blue' | 'yellow' }) => {
const colors = {
red: 'text-red-500 bg-red-500/10',
blue: 'text-blue-500 bg-blue-500/10',
Expand Down
9 changes: 5 additions & 4 deletions Client/src/components/MissionsHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface Mission {
difficulty: 'easy' | 'medium' | 'hard';
requirement_type?: string;
status: 'in_progress' | 'pending_verification' | 'completed';
progress: any;
progress: Record<string, unknown> | null;
time_remaining_ms: number;
}

Expand Down Expand Up @@ -215,7 +215,7 @@ type ViewType = 'all' | 'my';

const MissionsHub = () => {
const [missions, setMissions] = useState<Mission[]>([]);
const [profile, setProfile] = useState<any>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
Expand Down Expand Up @@ -274,10 +274,11 @@ const MissionsHub = () => {
setSolvingMissionId(null);
setProofLink('');
fetchData();
} catch (err: any) {
} catch (err) {
const message = err instanceof Error ? err.message : 'Could not verify mission.'
toast({
title: 'SYSTEM ERROR',
description: err?.message || 'Could not verify mission.',
description: message,
variant: 'destructive',
});
} finally {
Expand Down
13 changes: 7 additions & 6 deletions Client/src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class ApiError extends Error {
constructor(
message: string,
public status: number,
public data?: any
public data?: unknown
) {
super(message);
this.name = 'ApiError';
Expand All @@ -33,9 +33,10 @@ interface RequestOptions extends RequestInit {
* Get authentication token from Clerk session
*/
async function getAuthToken(): Promise<string | null> {
if (typeof window !== 'undefined' && (window as any).Clerk && (window as any).Clerk.session) {
const clerk = typeof window !== 'undefined' ? (window as Window & { Clerk?: { session?: { getToken?: () => Promise<string> } } }).Clerk : undefined
if (clerk?.session?.getToken) {
try {
return await (window as any).Clerk.session.getToken();
return await clerk.session.getToken();
} catch (e) {
console.warn("Failed to get Clerk token", e);
return null;
Expand Down Expand Up @@ -157,7 +158,7 @@ export const api = {
/**
* POST request
*/
post: <T>(endpoint: string, data?: any) =>
post: <T>(endpoint: string, data?: unknown) =>
request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
Expand All @@ -166,7 +167,7 @@ export const api = {
/**
* PATCH request
*/
patch: <T>(endpoint: string, data?: any) =>
patch: <T>(endpoint: string, data?: unknown) =>
request<T>(endpoint, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
Expand All @@ -175,7 +176,7 @@ export const api = {
/**
* PUT request
*/
put: <T>(endpoint: string, data?: any) =>
put: <T>(endpoint: string, data?: unknown) =>
request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
Expand Down
49 changes: 33 additions & 16 deletions Client/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,48 @@
import { api } from '@/lib/api-client';
import type { AuthResponse } from '@/types/api';

type ClerkUser = {
id: string
primaryEmailAddress?: { emailAddress: string } | null
fullName?: string | null
imageUrl?: string
username?: string | null
}

type AppUser = {
id: string
email?: string
full_name: string
avatar_url?: string
username?: string | null
}

const getClerk = () => (typeof window !== 'undefined' ? (window as Window & { Clerk?: { user?: ClerkUser; session?: { signOut?: () => Promise<void> }; signOut?: () => Promise<void> } }).Clerk : undefined)

export const authService = {
signUp: async (data: any): Promise<AuthResponse> => {
signUp: async (_data: unknown): Promise<AuthResponse> => {
throw new Error('Please use Clerk <SignUp /> component instead');
},

signIn: async (data: any): Promise<AuthResponse> => {
signIn: async (_data: unknown): Promise<AuthResponse> => {
throw new Error('Please use Clerk <SignIn /> component instead');
},

signOut: async (): Promise<void> => {
if (typeof window !== 'undefined' && (window as any).Clerk) {
await (window as any).Clerk.signOut();
const clerk = getClerk()
if (clerk?.signOut) {
await clerk.signOut();
}
},

logout: async function() {
await this.signOut();
},

getUser: (): any | null => {
if (typeof window !== 'undefined' && (window as any).Clerk) {
const user = (window as any).Clerk.user;
getUser: (): AppUser | null => {
const clerk = getClerk()
if (clerk?.user) {
const user = clerk.user;
if (!user) return null;
return {
id: user.id,
Expand All @@ -42,23 +62,20 @@ export const authService = {
return null;
},

updateUser: (userData: any): void => {
updateUser: (_userData: unknown): void => {
// Left empty. Profile modifications should happen via Clerk Dashboard or Clerk UI Components
console.warn('Profile modifications should be done via Clerk');
},

forgotPassword: async (data: any): Promise<void> => {},
verifyOTP: async (data: any): Promise<void> => {},
forgotPassword: async (_data: unknown): Promise<void> => {},
verifyOTP: async (_data: unknown): Promise<void> => {},

isAuthenticated: (): boolean => {
if (typeof window !== 'undefined' && (window as any).Clerk) {
return !!(window as any).Clerk.session;
}
return false;
return Boolean(getClerk()?.session);
},

signInWithProvider: async (provider: 'github' | 'google'): Promise<void> => {},
sendMagicLink: async (email: string): Promise<void> => {},
verifyMagicLink: async (email: string, otp: string): Promise<any> => ({}),
resetPassword: async (data: any): Promise<void> => {},
verifyMagicLink: async (_email: string, _otp: string): Promise<Record<string, never>> => ({}),
resetPassword: async (_data: unknown): Promise<void> => {},
};
53 changes: 53 additions & 0 deletions backend/lib/supabase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createClient as createSupabaseClient } from '@supabase/supabase-js'

type QueryResult<T> = {
data: T | null
error: Error | null
}

const getRequiredEnv = (name: string) => {
const value = process.env[name]
if (!value) {
throw new Error('Missing Supabase environment variables')
}
return value
}

export function getServerClient() {
const url = getRequiredEnv('NEXT_PUBLIC_SUPABASE_URL')
const serviceRoleKey = getRequiredEnv('SUPABASE_SERVICE_ROLE_KEY')

return createSupabaseClient(url, serviceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
},
})
}

export function getClientSupabase() {
const url = getRequiredEnv('NEXT_PUBLIC_SUPABASE_URL')
const anonKey = getRequiredEnv('NEXT_PUBLIC_SUPABASE_ANON_KEY')

return createSupabaseClient(url, anonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
},
})
}

export async function executeQuery<T>(
queryFn: (client: ReturnType<typeof getServerClient>) => Promise<T>
): Promise<QueryResult<T>> {
try {
const data = await queryFn(getServerClient())
return { data, error: null }
} catch (error) {
return {
data: null,
error: error instanceof Error ? error : new Error('Unknown database error'),
}
}
}

3 changes: 2 additions & 1 deletion backend/lib/validations/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const leaderboardUpdateSchema = z.object({
.string()
.uuid('Event ID must be a valid UUID'),
user_id: z
.string(),
.string()
.uuid('User ID must be a valid UUID'),
score: z
.number()
.int('Score must be an integer')
Expand Down
12 changes: 6 additions & 6 deletions backend/lib/validations/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,30 @@ export const profileUpdateSchema = z.object({
.or(z.literal('')),
github_url: z
.string()
.url('GitHub URL must be a valid URL')
.regex(/^https?:\/\/.+/i, 'GitHub URL must be a valid URL')
.optional()
.or(z.literal('')),
linkedin_url: z
.string()
.url('LinkedIn URL must be a valid URL')
.regex(/^https?:\/\/.+/i, 'LinkedIn URL must be a valid URL')
.optional()
.or(z.literal('')),
portfolio_url: z
.string()
.url('Portfolio URL must be a valid URL')
.regex(/^https?:\/\/.+/i, 'Portfolio URL must be a valid URL')
.optional()
.or(z.literal('')),
skills: z
.array(z.string())
.max(15, 'Cannot have more than 15 skills')
.max(10, 'Cannot have more than 10 skills')
.optional(),
university: z.string().max(100).optional().or(z.literal('')),
education: z.string().max(100).optional().or(z.literal('')),
graduation_year: z.union([z.number().int().min(1900).max(2100), z.string(), z.null()]).optional(),
phone: z.string().max(20).optional().or(z.literal('')),
address: z.string().max(200).optional().or(z.literal('')),
avatar_url: z.string().url().optional().or(z.literal('')),
banner_url: z.string().url().optional().or(z.literal('')),
banner_url: z.string().regex(/^https?:\/\/.+/i, 'Banner URL must be a valid URL').optional().or(z.literal('')),
interests: z.array(z.string()).max(10, 'Cannot have more than 10 interests').optional(),
first_name: z.string().max(50).optional().or(z.literal('')),
last_name: z.string().max(50).optional().or(z.literal('')),
Expand All @@ -60,7 +60,7 @@ export const profileUpdateSchema = z.object({
roles: z.array(z.string()).optional(),
resume_url: z.string().url().optional().or(z.literal('')),
has_experience: z.boolean().optional(),
twitter_url: z.string().url().optional().or(z.literal('')),
twitter_url: z.string().regex(/^https?:\/\/.+/i, 'Twitter URL must be a valid URL').optional().or(z.literal('')),
emergency_contact_name: z.string().max(100).optional().or(z.literal('')),
emergency_contact_phone: z.string().max(20).optional().or(z.literal('')),
is_email_public: z.boolean().optional(),
Expand Down
2 changes: 1 addition & 1 deletion backend/lib/validations/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const resourceCreateSchema = z.object({
.max(1000, 'Description must not exceed 1000 characters'),
content_url: z
.string()
.url('Content URL must be a valid URL'),
.regex(/^https?:\/\/.+/i, 'Content URL must be a valid URL'),
category: z
.string()
.min(1, 'Category is required')
Expand Down
Loading
Loading