diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..533b3ad --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,24 @@ +import { apiRequest } from './client'; +import type { AuthUser } from '../types/User.ts'; + +export const authApi = { + // Get current user + getMe: () => apiRequest('/api/v1/auth/me'), + + // Initiate Auth + login: (provider: string) => { + window.location.href = `/api/v1/auth/${provider}`; + }, + + // Logout + logout: () => + apiRequest('/api/v1/auth/logout', { + method: 'POST', + }), + + // Refresh token + refresh: () => + apiRequest('/api/v1/auth/refresh', { + method: 'POST', + }), +}; diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..4bc4d78 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,38 @@ +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; + +// All fields are optional since there are defaults +interface RequestOptions { + method?: HttpMethod; + body?: TBody; + headers?: HeadersInit; + credentials?: RequestCredentials; +} + +// TResponse: expected response from the server +// TBody: expected shape of the body (default: unknown) +// Params: endpoint and RequestOptions +export async function apiRequest( + endpoint: string, + options: RequestOptions = {} +): Promise { + const { method = 'GET', body, headers = {}, credentials = 'include' } = options; // defaults + + const response = await fetch(endpoint, { + method, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + credentials, + body: body ? JSON.stringify(body) : undefined, + }); + + // Error handling + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API Error: ${response.status} - ${errorText}`); + } + + // Success + return response.json(); +} diff --git a/src/api/events.ts b/src/api/events.ts new file mode 100644 index 0000000..47e53ff --- /dev/null +++ b/src/api/events.ts @@ -0,0 +1,71 @@ +import { apiRequest } from './client'; +import type { CreateEvent, UpdateEvent, RegisterEvent, Event, EventUser } from '../types/Event.ts'; +import type { QueryParams } from '../types/QueryParams.ts'; + +export const eventsApi = { + // Get all events + getAll: (params?: QueryParams) => { + const query = new URLSearchParams(); + if (params?.limit !== undefined) { + query.append('limit', params.limit.toString()); + } + if (params?.offset !== undefined) { + query.append('offset', params.offset.toString()); + } + const queryString = query.toString(); + return apiRequest(queryString ? `/events?${queryString}` : '/events'); + }, + + // Create an event + create: (data: CreateEvent) => + apiRequest(`/events`, { + method: 'POST', + body: data, + }), + + // List events by organization + getFromOrganization: (oid: string, params?: QueryParams) => { + const query = new URLSearchParams(); + if (params?.limit !== undefined) { + query.append('limit', params.limit.toString()); + } + if (params?.offset !== undefined) { + query.append('offset', params.offset.toString()); + } + const queryString = query.toString(); + return apiRequest(queryString ? `/events/org/${oid}?${queryString}` : `/events/org/${oid}`); + }, + + // Get event + getById: (eid: string) => apiRequest(`/events/${eid}`), + + // Update event + update: (eid: string, data: UpdateEvent) => + apiRequest(`/events/${eid}`, { + method: 'PUT', + body: data, + }), + + // Delete event + delete: (eid: string) => + apiRequest(`/events/${eid}`, { + method: 'DELETE', + }), + + // Register for event + register: (eid: string, data: RegisterEvent) => + apiRequest(`/events/${eid}/register`, { + method: 'POST', + body: data, + }), + + // Unregister from event + unregister: (eid: string, data: RegisterEvent) => + apiRequest(`/events/${eid}/register`, { + method: 'DELETE', + body: data, + }), + + // Get users registered for an event + getUsers: (eid: string) => apiRequest(`/events/${eid}/registrations`), +}; diff --git a/src/api/organizations.ts b/src/api/organizations.ts new file mode 100644 index 0000000..d02b019 --- /dev/null +++ b/src/api/organizations.ts @@ -0,0 +1,79 @@ +import { apiRequest } from './client'; +import type { + Organization, + CreateOrganization, + UpdateOrganization, + OrganizationUser, + ManageOrganizationUser, +} from '../types/Organization.ts'; +import type { Event } from '../types/Event.ts'; +import type { QueryParams } from '../types/QueryParams.ts'; + +export const organizationsApi = { + // Get all organizations + getAll: (params?: QueryParams) => { + const query = new URLSearchParams(); + if (params?.limit !== undefined) { + query.append('limit', params.limit.toString()); + } + if (params?.offset !== undefined) { + query.append('offset', params.offset.toString()); + } + const queryString = query.toString(); + return apiRequest(queryString ? `/organizations?${queryString}` : '/organizations'); + }, + + // Create organization + create: (data: CreateOrganization) => + apiRequest(`/events`, { + method: 'POST', + body: data, + }), + + // Get organization + getById: (oid: string) => apiRequest(`/organizations/${oid}`), + + // Update organization + update: (oid: string, data: UpdateOrganization) => + apiRequest(`/organizations/${oid}`, { + method: 'PUT', + body: data, + }), + + // Delete organization + delete: (oid: string) => + apiRequest(`/organizations/${oid}`, { + method: 'DELETE', + }), + + // Get organization's events + events: (oid: string, params?: QueryParams) => { + const query = new URLSearchParams(); + if (params?.limit !== undefined) { + query.append('limit', params.limit.toString()); + } + if (params?.offset !== undefined) { + query.append('offset', params.offset.toString()); + } + const queryString = query.toString(); + return apiRequest( + queryString ? `/organizations/${oid}/events?${queryString}` : `/organizations/${oid}/events` + ); + }, + // Get organization's members + getMembers: (oid: string) => apiRequest(`/organizations/${oid}/members`), + + // Add member to organization + addMember: (oid: string, data: ManageOrganizationUser) => + apiRequest(`/organizations/${oid}/members`, { + method: 'POST', + body: data, + }), + + // Remove organization member + deleteMember: (oid: string, data: ManageOrganizationUser) => + apiRequest(`/organizations/${oid}/members`, { + method: 'DELETE', + body: data, + }), +}; diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 0000000..4ba3cec --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,28 @@ +import { apiRequest } from './client'; +import type { User } from '../types/User.ts'; +import type { Event } from '../types/Event.ts'; +import type { Organization } from '../types/Organization.ts'; + +export const usersApi = { + // Get user by ID + getById: (id: string) => apiRequest(`/users/${id}`), + + // Update user + update: (id: string, data: User) => + apiRequest(`/users/${id}`, { + method: 'POST', + body: data, + }), + + // Delete user + delete: (id: string) => + apiRequest(`/users/${id}`, { + method: 'DELETE', + }), + + // Get user's events + getEvents: (id: string) => apiRequest(`/users/${id}/events`), + + // Get user's organizations + getOrganizations: (id: string) => apiRequest(`/users/${id}/organizations`), +}; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 5f623fe..fc627e3 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-refresh/only-export-components*/ -import React, { createContext, useContext, useState, type ReactNode } from 'react'; +import React, { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; +import { authApi } from '../api/auth'; type User = { uid?: string; @@ -23,7 +24,6 @@ type AuthProviderProps = { }; const AuthContext = createContext(undefined); -const AUTH_API_BASE = '/api/v1/auth'; export const AuthProvider: React.FC = ({ children }) => { const [user, setUser] = useState(null); @@ -33,77 +33,42 @@ export const AuthProvider: React.FC = ({ children }) => { const fetchMe = async (): Promise => { try { setLoading(true); + setError(null); - const response = await fetch(`${AUTH_API_BASE}/me`, { - headers: { Accept: 'application/json' }, - credentials: 'include', - }); - - if (response.ok) { - const data: User = await response.json(); - setUser(data); - } else { - setUser(null); - } + const data = await authApi.getMe(); + setUser(data); } catch (err: unknown) { - if (err instanceof Error) { - console.error('Failed to fetch user:', err); - setError(err.message); - } else { - console.error('Unknown error:', err); - setError('Unknown error'); - } - setUser(null); + setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setLoading(false); } }; + useEffect(() => { + fetchMe(); + + const timeout = setTimeout(() => { + fetchMe(); + }, 1000); + + return () => clearTimeout(timeout); + }, []); + const login = (provider: 'google' | 'microsoft'): void => { - const url = provider === 'google' ? `${AUTH_API_BASE}/google` : `${AUTH_API_BASE}/microsoft`; - - window.open(url, '_blank'); - - const pollInterval = setInterval(async () => { - try { - const response = await fetch(`${AUTH_API_BASE}/me`, { - headers: { Accept: 'application/json' }, - credentials: 'include', - }); - - if (response.ok) { - const data: User = await response.json(); - setUser(data); - clearInterval(pollInterval); - } - } catch (err: unknown) { - if (err instanceof Error) { - console.error('Polling failed:', err.message); - } - } - }, 3000); - - setTimeout(() => { - clearInterval(pollInterval); - }, 120000); + authApi.login(provider); }; + const logout = async (): Promise => { try { - await fetch(`${AUTH_API_BASE}/logout`, { - method: 'POST', - credentials: 'include', - }); - + await authApi.logout(); setUser(null); - - window.location.href = '/app/'; + window.location.href = '/app'; } catch (err: unknown) { - if (err instanceof Error) { - console.error('Logout failed:', err.message); - } + console.error('Logout failed:', err); } }; + const value: AuthContextType = { user, loading, diff --git a/src/pages/Events.tsx b/src/pages/Events.tsx index 33ad043..6df39e4 100644 --- a/src/pages/Events.tsx +++ b/src/pages/Events.tsx @@ -25,104 +25,104 @@ export default function Events() { // SAMPLE DATA const myEvents: Event[] = [ { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 1 This is event 1 This is event 1 This is event 1', eid: '1', - event_time: new Date(), + event_time: '7:00pm', location: 'Amos Eaton', - organization: 'Chorus', - title: 'Sing-along', + //organization: 'Chorus', + //title: 'Sing-along', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 2 This is event 2 This is event 2 This is event 2', eid: '2', - event_time: new Date(), + event_time: '7:00pm', location: 'VCC', - organization: 'Coding', - title: 'Hackathon', + //organization: 'Coding', + //title: 'Hackathon', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 3 This is event 3 This is event 3 This is event 3', eid: '3', - event_time: new Date(), + event_time: '7:00pm', location: 'Sage', - organization: 'Dance', - title: 'Competition', + //organization: 'Dance', + //title: 'Competition', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 4 This is event 4 This is event 4 This is event 4', eid: '4', - event_time: new Date(), + event_time: '7:00pm', location: 'Folsom', - organization: 'Math', - title: 'Review Session', + //organization: 'Math', + //title: 'Review Session', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 5 This is event 5 This is event 5 This is event 5', eid: '5', - event_time: new Date(), + event_time: '7:00pm', location: 'EMPAC', - organization: 'AGT', - title: 'America Got Talent', + //organization: 'AGT', + //title: 'America Got Talent', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 6 This is event 6 This is event 6 This is event 6', eid: '6', - event_time: new Date(), + event_time: '7:00pm', location: 'Sage Dining Hall', - organization: 'CCPD', - title: 'Networking Session', + //organization: 'CCPD', + //title: 'Networking Session', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 7 This is event 7 This is event 7 This is event 7', eid: '7', - event_time: new Date(), + event_time: '7:00pm', location: 'EMPAC', - organization: 'AGT', - title: 'America Got Talent', + //organization: 'AGT', + //title: 'America Got Talent', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 8 This is event 8 This is event 8 This is event 8', eid: '8', - event_time: new Date(), + event_time: '7:00pm', location: 'Carnegie', - organization: 'Math Club', - title: 'Math Competition', + //organization: 'Math Club', + //title: 'Math Competition', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 9 This is event 9 This is event 9 This is event 9', eid: '9', - event_time: new Date(), + event_time: '7:00pm', location: 'MRC', - organization: 'Materials Research', - title: 'Guest Speaker', + //organization: 'Materials Research', + //title: 'Guest Speaker', }, { - date_created: new Date(), - date_modified: new Date(), + date_created: '03/24/2026', + date_modified: '03/24/2026', description: 'This is event 10 This is event 10 This is event 10 This is event 10', eid: '10', - event_time: new Date(), + event_time: '7:00pm', location: 'DCC 308', - organization: 'CCPD', - title: 'Pitch Competition', + //organization: 'CCPD', + //title: 'Pitch Competition', }, ]; diff --git a/src/pages/Organizations.tsx b/src/pages/Organizations.tsx index 1cb9c9f..5e604d5 100644 --- a/src/pages/Organizations.tsx +++ b/src/pages/Organizations.tsx @@ -22,35 +22,35 @@ export default function Organizations() { { date_created: new Date(), date_modified: new Date(), - description: 'This is organization 1 This is organization 1 This is organization 1', + //description: 'This is organization 1 This is organization 1 This is organization 1', name: 'CAPY', oid: '1', }, { date_created: new Date(), date_modified: new Date(), - description: 'This is organization 2 This is organization 2 This is organization 2', + //description: 'This is organization 2 This is organization 2 This is organization 2', name: 'LXA', oid: '2', }, { date_created: new Date(), date_modified: new Date(), - description: 'This is organization 3 This is organization 3 This is organization 3', + //description: 'This is organization 3 This is organization 3 This is organization 3', name: 'RPAI', oid: '3', }, { date_created: new Date(), date_modified: new Date(), - description: 'This is organization 4 This is organization 4 This is organization 4', + //description: 'This is organization 4 This is organization 4 This is organization 4', name: 'Robotics', oid: '4', }, { date_created: new Date(), date_modified: new Date(), - description: 'This is organization 5 This is organization 5 This is organization 5', + //description: 'This is organization 5 This is organization 5 This is organization 5', name: 'RPISEC', oid: '5', }, diff --git a/src/types/Event.ts b/src/types/Event.ts index 3d6c22e..6ccafb1 100644 --- a/src/types/Event.ts +++ b/src/types/Event.ts @@ -1,10 +1,37 @@ export type Event = { - date_created: Date; - date_modified: Date; + date_created: string; + date_modified: string; description: string; eid: string; - event_time: Date; + event_time: string; location: string; - organization: string; - title: string; + // organization: string; + // title: string; +}; + +export type CreateEvent = { + description: string; + event_time: string; + location: string; + org_id: string; +}; + +export type UpdateEvent = { + description: string; + event_time: string; + location: string; +}; + +export type RegisterEvent = { + is_attending: boolean; + uid: string; +}; + +export type EventUser = { + date_registered: string; + first_name: string; + is_admin: boolean; + is_attending: boolean; + last_name: string; + uid: string; }; diff --git a/src/types/Organization.ts b/src/types/Organization.ts index bb56ed2..ad307ae 100644 --- a/src/types/Organization.ts +++ b/src/types/Organization.ts @@ -1,7 +1,31 @@ export type Organization = { date_created: Date; date_modified: Date; - description: string; + //description: string; name: string; oid: string; }; + +export type CreateOrganization = { + creator_uid: string; + name: string; +}; + +export type UpdateOrganization = { + name: string; +}; + +export type OrganizationUser = { + date_joined: string; + email: string; + first_name: string; + is_admin: boolean; + last_active: string; + last_name: string; + uid: string; +}; + +export type ManageOrganizationUser = { + is_admin: boolean; + uid: string; +}; diff --git a/src/types/QueryParams.ts b/src/types/QueryParams.ts new file mode 100644 index 0000000..d892d25 --- /dev/null +++ b/src/types/QueryParams.ts @@ -0,0 +1,4 @@ +export type QueryParams = { + limit?: number; + offset?: number; +}; diff --git a/src/types/User.ts b/src/types/User.ts new file mode 100644 index 0000000..ed9cd37 --- /dev/null +++ b/src/types/User.ts @@ -0,0 +1,20 @@ +export type User = { + date_created: string; + date_modified: string; + first_name: string; + grad_year: number; + last_name: string; + personal_email: string; + phone: string; + role: string; + school_email: string; + uid: string; +}; + +export type AuthUser = { + email: string; + first_name: string; + last_name: string; + role: string; + uid: string; +}; diff --git a/vite.config.ts b/vite.config.ts index 617726c..06e858f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig(() => { return { plugins: [react()], - base: '/app/', + base: '/app', server: { port: 5173, proxy: {