- {/* Stats Grid */}
- {/* Hackathons */}
@@ -76,7 +78,6 @@ export default function OrganizationCard({
- {/* Grants */}
@@ -107,14 +108,10 @@ export default function OrganizationCard({
- {/* Footer */}
-
+
Manage Organization
-
+
);
}
diff --git a/components/organization/tabs/LinksTab.tsx b/components/organization/tabs/LinksTab.tsx
index 44956fb45..812cbc9a5 100644
--- a/components/organization/tabs/LinksTab.tsx
+++ b/components/organization/tabs/LinksTab.tsx
@@ -1,15 +1,18 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { BoundlessButton } from '@/components/buttons';
+import { useOrganization } from '@/lib/providers/OrganizationProvider';
+import { cn } from '@/lib/utils';
+import { toast } from 'sonner';
export interface OrganizationLinks {
website: string;
- twitter: string;
+ x: string;
github: string;
- other: string;
+ others: string;
}
interface LinksTabProps {
@@ -18,22 +21,121 @@ interface LinksTabProps {
}
export default function LinksTab({
- initialLinks = { website: '', twitter: '', github: '', other: '' },
+ initialLinks = { website: '', x: '', github: '', others: '' },
onSave,
}: LinksTabProps) {
+ const { activeOrgId, activeOrg, updateOrganizationLinks, isLoading } =
+ useOrganization();
+
const [links, setLinks] = useState
(initialLinks);
+ const [hasUserChanges, setHasUserChanges] = useState(false);
+ const [isInitialized, setIsInitialized] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+
+ useEffect(() => {
+ if (!isInitialized) {
+ if (activeOrg?.links) {
+ setLinks({
+ website: activeOrg.links.website || '',
+ x: activeOrg.links.x || '',
+ github: activeOrg.links.github || '',
+ others: activeOrg.links.others || '',
+ });
+ } else if (initialLinks) {
+ setLinks(initialLinks);
+ }
+ setHasUserChanges(false);
+ setIsInitialized(true);
+ }
+ }, [activeOrg, initialLinks, isInitialized]);
+
+ useEffect(() => {
+ if (!isInitialized || hasUserChanges) return;
+ if (!activeOrg?.links) return;
+
+ const newLinks = {
+ website: activeOrg.links.website || '',
+ x: activeOrg.links.x || '',
+ github: activeOrg.links.github || '',
+ others: activeOrg.links.others || '',
+ };
+
+ const currentLinksStr = JSON.stringify(links);
+ const newLinksStr = JSON.stringify(newLinks);
+
+ if (newLinksStr !== currentLinksStr) {
+ setLinks(newLinks);
+ setHasUserChanges(false);
+ }
+ }, [activeOrg?.links, isInitialized, hasUserChanges, links]);
const updateLink = (field: keyof OrganizationLinks, value: string) => {
setLinks(prev => ({ ...prev, [field]: value }));
+ setHasUserChanges(true);
};
- const handleSave = () => {
- onSave?.(links);
+ const handleSave = async () => {
+ if (!activeOrgId) {
+ toast.error('No active organization selected');
+ return;
+ }
+
+ setIsSaving(true);
+
+ try {
+ if (!updateOrganizationLinks) {
+ throw new Error(
+ 'Organization functions not available. Please refresh the page.'
+ );
+ }
+
+ await updateOrganizationLinks(activeOrgId, links);
+
+ toast.success('Organization links updated successfully');
+ setHasUserChanges(false);
+
+ if (onSave) {
+ onSave(links);
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ if (
+ error.message.includes('401') ||
+ error.message.includes('Unauthorized')
+ ) {
+ toast.error('Authentication failed. Please login again.');
+ } else if (
+ error.message.includes('403') ||
+ error.message.includes('Forbidden')
+ ) {
+ toast.error(
+ 'You do not have permission to update this organization.'
+ );
+ } else if (
+ error.message.includes('404') ||
+ error.message.includes('Not Found')
+ ) {
+ toast.error('Organization not found.');
+ } else if (
+ error.message.includes('Network') ||
+ error.message.includes('timeout')
+ ) {
+ toast.error(
+ 'Network error. Please check your connection and try again.'
+ );
+ } else {
+ toast.error(`Failed to save organization links: ${error.message}`);
+ }
+ } else {
+ toast.error('Failed to save organization links. Please try again.');
+ }
+ } finally {
+ setIsSaving(false);
+ }
};
return (
- {/* Website */}
Website
updateLink('website', e.target.value)}
placeholder='Enter link to organization website'
className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ disabled={isSaving}
/>
- {/* X (Twitter) */}
- {/* GitHub */}
GitHub
updateLink('github', e.target.value)}
placeholder='Link to GitHub repo or GitHub organization profile'
className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ disabled={isSaving}
/>
- {/* Other Link */}
Other Link (optional)
updateLink('other', e.target.value)}
+ value={links.others}
+ onChange={e => updateLink('others', e.target.value)}
placeholder='Link URL (newsletters or social account)'
className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ disabled={isSaving}
/>
- {/* Save Button */}
-
- Save Changes
-
+
+ {hasUserChanges && (
+
+
+ You have unsaved changes
+
+ )}
+
+ {isSaving ? 'Saving...' : 'Save Changes'}
+
+
);
}
diff --git a/components/organization/tabs/MembersTab.tsx b/components/organization/tabs/MembersTab.tsx
index e0b9787e8..87ff15f9a 100644
--- a/components/organization/tabs/MembersTab.tsx
+++ b/components/organization/tabs/MembersTab.tsx
@@ -1,7 +1,8 @@
'use client';
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
import { BoundlessButton } from '@/components/buttons';
+import { useOrganization } from '@/lib/providers/OrganizationProvider';
import EmailInviteSection from './MembersTab/EmailInviteSection';
import PermissionsTable from './MembersTab/PermissionsTable';
import TeamManagementSection from './MembersTab/TeamManagementSection';
@@ -17,96 +18,65 @@ interface Member {
}
interface MembersTabProps {
- initialMembers?: Member[];
onSave?: (members: Member[]) => void;
}
-export default function MembersTab({
- initialMembers = [],
- onSave,
-}: MembersTabProps) {
- const dummyMembers: Member[] = [
- {
- id: 'member-1',
- name: 'Sarah Johnson',
- email: 'sarah.johnson@example.com',
- avatar: '/avatar.png',
- role: 'admin',
- joinedAt: new Date().toISOString(),
- status: 'active',
- },
- {
- id: 'member-2',
- name: 'Mike Chen',
- email: 'mike.chen@example.com',
- avatar: '/avatar.png',
+export default function MembersTab({ onSave }: MembersTabProps) {
+ const {
+ activeOrg,
+ activeOrgId,
+ updateOrganizationMembers,
+ inviteMember,
+ removeMember,
+ isLoading,
+ } = useOrganization();
+
+ const members: Member[] = useMemo(() => {
+ const emails = activeOrg?.members ?? [];
+ return emails.map((email, idx) => ({
+ id: `${idx}-${email}`,
+ name: email.split('@')[0] || email,
+ email,
role: 'member',
joinedAt: new Date().toISOString(),
status: 'active',
- },
- {
- id: 'member-3',
- name: 'Emily Rodriguez',
- email: 'emily.rodriguez@example.com',
- avatar: '/avatar.png',
- role: 'admin',
- joinedAt: new Date().toISOString(),
- status: 'active',
- },
- {
- id: 'pending-1',
- name: 'Alex Thompson',
- email: 'alex.thompson@example.com',
- avatar: '/avatar.png',
- role: 'member',
- joinedAt: new Date().toISOString(),
- status: 'pending',
- },
- {
- id: 'pending-2',
- name: 'Lisa Wang',
- email: 'lisa.wang@example.com',
- avatar: '/avatar.png',
- role: 'admin',
- joinedAt: new Date().toISOString(),
- status: 'pending',
- },
- ];
-
- const [members, setMembers] = useState(
- initialMembers.length > 0 ? initialMembers : dummyMembers
- );
+ }));
+ }, [activeOrg?.members]);
const [inviteEmails, setInviteEmails] = useState([]);
const [emailInput, setEmailInput] = useState('');
+ const [hasUserChanges, setHasUserChanges] = useState(false);
const handleInvite = () => {
- if (inviteEmails.length > 0) {
- const newMembers: Member[] = inviteEmails.map(email => ({
- id: Date.now().toString() + Math.random(),
- name: email.split('@')[0],
- email: email,
- role: 'member', // Default role
- joinedAt: new Date().toISOString(),
- status: 'pending',
- }));
- setMembers([...members, ...newMembers]);
+ if (inviteEmails.length > 0 && activeOrgId) {
+ inviteMember(activeOrgId, inviteEmails);
setInviteEmails([]);
setEmailInput('');
}
};
- const handleRoleChange = (memberId: string, newRole: string) => {
- setMembers(prev =>
- prev.map(member =>
- member.id === memberId
- ? { ...member, role: newRole as 'admin' | 'member' }
- : member
- )
- );
+ const handleRoleChange = (memberId: string) => {
+ if (!activeOrgId) return;
+ const m = members.find(x => x.id === memberId);
+ if (!m) return;
+ updateOrganizationMembers(activeOrgId, [m.email]);
+ setHasUserChanges(true);
};
const handleRemoveMember = (memberId: string) => {
- setMembers(prev => prev.filter(member => member.id !== memberId));
+ if (!activeOrgId) return;
+ const m = members.find(x => x.id === memberId);
+ if (!m) return;
+ removeMember(activeOrgId, m.email);
+ };
+
+ const handleSave = async () => {
+ if (!activeOrgId) return;
+ const emails = members.map(m => m.email);
+ try {
+ await updateOrganizationMembers(activeOrgId, emails);
+ onSave?.(members);
+ setHasUserChanges(false);
+ } catch {}
};
return (
@@ -127,9 +97,21 @@ export default function MembersTab({
onRemoveMember={handleRemoveMember}
/>
- onSave?.(members)} className=''>
- Save Changes
-
+
+ {hasUserChanges && (
+
+
+ You have unsaved changes
+
+ )}
+
+ {isLoading ? 'Saving...' : 'Save Changes'}
+
+
);
}
diff --git a/components/organization/tabs/MembersTab/TeamManagementSection.tsx b/components/organization/tabs/MembersTab/TeamManagementSection.tsx
index 4197d2cac..74a1952d2 100644
--- a/components/organization/tabs/MembersTab/TeamManagementSection.tsx
+++ b/components/organization/tabs/MembersTab/TeamManagementSection.tsx
@@ -24,7 +24,6 @@ export default function TeamManagementSection({
onRoleChange,
onRemoveMember,
}: TeamManagementSectionProps) {
- // Always show owner as the first member
const ownerMember = {
id: 'owner-1',
name: 'Collins Chikangwu',
@@ -35,7 +34,6 @@ export default function TeamManagementSection({
status: 'active' as const,
};
- // Separate members by status
const activeMembers = members.filter(member => member.status === 'active');
const pendingMembers = members.filter(member => member.status === 'pending');
@@ -51,7 +49,6 @@ export default function TeamManagementSection({
- {/* Owner - Always displayed */}
@@ -68,7 +65,6 @@ export default function TeamManagementSection({
- {/* Active Members (excluding owner) */}
{activeMembers.filter(member => member.role !== 'owner').length > 0 && (
{activeMembers
@@ -84,7 +80,6 @@ export default function TeamManagementSection({
)}
- {/* Pending Invites */}
{pendingMembers.length > 0 && (
Pending Invites
diff --git a/components/organization/tabs/ProfileTab.tsx b/components/organization/tabs/ProfileTab.tsx
index 9c39779c2..c9749cb71 100644
--- a/components/organization/tabs/ProfileTab.tsx
+++ b/components/organization/tabs/ProfileTab.tsx
@@ -1,13 +1,17 @@
'use client';
-import { useState } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
+import Image from 'next/image';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
-import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import FormHint from '../../form/FormHint';
import MDEditor from '@uiw/react-md-editor';
+import { useOrganization } from '@/lib/providers/OrganizationProvider';
+import { BoundlessButton } from '@/components/buttons';
+import { toast } from 'sonner';
+import { uploadService } from '@/lib/api/upload';
interface ProfileTabProps {
initialData?: {
@@ -20,35 +24,318 @@ interface ProfileTabProps {
}
export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
+ const {
+ activeOrgId,
+ activeOrg,
+ createOrganization,
+ updateOrganization,
+ isLoading,
+ } = useOrganization();
+
const [formData, setFormData] = useState({
- name: initialData?.name || '',
- logo: initialData?.logo || '',
- tagline: initialData?.tagline || '',
- about: initialData?.about || '',
+ name: '',
+ logo: '',
+ tagline: '',
+ about: '',
});
- const handleSave = () => {
- onSave?.(formData);
+ const [isSaving, setIsSaving] = useState(false);
+ const [logoPreview, setLogoPreview] = useState
('');
+ const [hasUserChanges, setHasUserChanges] = useState(false);
+ const [isInitialized, setIsInitialized] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadError, setUploadError] = useState(null);
+ const [isDragOver, setIsDragOver] = useState(false);
+ const fileInputRef = useRef(null);
+ const previousOrgIdRef = useRef(null);
+
+ useEffect(() => {
+ if (!isInitialized) {
+ if (activeOrg) {
+ setFormData({
+ name: activeOrg.name || '',
+ logo: activeOrg.logo || '',
+ tagline: activeOrg.tagline || '',
+ about: activeOrg.about || '',
+ });
+ setLogoPreview(activeOrg.logo || '');
+ setHasUserChanges(false);
+ setIsInitialized(true);
+ previousOrgIdRef.current = activeOrg._id;
+ } else if (initialData) {
+ setFormData({
+ name: initialData.name || '',
+ logo: initialData.logo || '',
+ tagline: initialData.tagline || '',
+ about: initialData.about || '',
+ });
+ setLogoPreview(initialData.logo || '');
+ setHasUserChanges(false);
+ setIsInitialized(true);
+ }
+ }
+ }, [activeOrg, initialData, isInitialized]);
+
+ useEffect(() => {
+ if (activeOrg && isInitialized) {
+ const currentOrgId = activeOrg._id;
+ const previousOrgId = previousOrgIdRef.current;
+
+ if (currentOrgId && currentOrgId !== previousOrgId) {
+ setFormData({
+ name: activeOrg.name || '',
+ logo: activeOrg.logo || '',
+ tagline: activeOrg.tagline || '',
+ about: activeOrg.about || '',
+ });
+ setLogoPreview(activeOrg.logo || '');
+ setHasUserChanges(false);
+ previousOrgIdRef.current = currentOrgId;
+ }
+ }
+ }, [activeOrg, isInitialized]);
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ setHasUserChanges(true);
+ };
+
+ const handleLogoUpload = async (
+ event: React.ChangeEvent
+ ) => {
+ const file = event.target.files?.[0];
+ if (file) {
+ if (!file.type.match(/^image\/(jpeg|jpg|png)$/)) {
+ toast.error('Please upload a JPEG or PNG image');
+ return;
+ }
+
+ if (file.size > 2 * 1024 * 1024) {
+ toast.error('File size must be less than 2MB');
+ return;
+ }
+
+ setUploadError(null);
+
+ const reader = new FileReader();
+ reader.onload = e => {
+ const result = e.target?.result as string;
+ setLogoPreview(result);
+ };
+ reader.readAsDataURL(file);
+
+ setIsUploading(true);
+ try {
+ const uploadResult = await uploadService.uploadSingle(file, {
+ folder: 'boundless/organizations/logos',
+ tags: ['organization', 'logo'],
+ transformation: {
+ width: 400,
+ height: 400,
+ crop: 'fit',
+ quality: 'auto',
+ format: 'auto',
+ },
+ });
+
+ if (uploadResult.success) {
+ setFormData(prev => ({
+ ...prev,
+ logo: uploadResult.data.secure_url,
+ }));
+ setHasUserChanges(true);
+ toast.success('Logo uploaded successfully');
+ } else {
+ throw new Error(uploadResult.message || 'Upload failed');
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Upload failed';
+ setUploadError(errorMessage);
+ toast.error(`Failed to upload logo: ${errorMessage}`);
+ } finally {
+ setIsUploading(false);
+ }
+ }
+ };
+
+ const handleDragOver = (event: React.DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setIsDragOver(true);
+ };
+
+ const handleDragLeave = (event: React.DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setIsDragOver(false);
+ };
+
+ const handleDrop = async (event: React.DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setIsDragOver(false);
+
+ const files = event.dataTransfer.files;
+ if (files && files.length > 0) {
+ const file = files[0];
+
+ if (!file.type.match(/^image\/(jpeg|jpg|png)$/)) {
+ toast.error('Please upload a JPEG or PNG image');
+ return;
+ }
+
+ if (file.size > 2 * 1024 * 1024) {
+ toast.error('File size must be less than 2MB');
+ return;
+ }
+
+ setUploadError(null);
+
+ const reader = new FileReader();
+ reader.onload = e => {
+ const result = e.target?.result as string;
+ setLogoPreview(result);
+ };
+ reader.readAsDataURL(file);
+
+ setIsUploading(true);
+ try {
+ const uploadResult = await uploadService.uploadSingle(file, {
+ folder: 'boundless/organizations/logos',
+ tags: ['organization', 'logo'],
+ transformation: {
+ width: 400,
+ height: 400,
+ crop: 'fit',
+ quality: 'auto',
+ format: 'auto',
+ },
+ });
+
+ if (uploadResult.success) {
+ setFormData(prev => ({
+ ...prev,
+ logo: uploadResult.data.secure_url,
+ }));
+ setHasUserChanges(true);
+ toast.success('Logo uploaded successfully');
+ } else {
+ throw new Error(uploadResult.message || 'Upload failed');
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Upload failed';
+ setUploadError(errorMessage);
+ toast.error(`Failed to upload logo: ${errorMessage}`);
+ } finally {
+ setIsUploading(false);
+ }
+ }
+ };
+
+ const handleSave = async () => {
+ if (!formData.name.trim()) {
+ toast.error('Organization name is required');
+ return;
+ }
+
+ if (!formData.tagline.trim()) {
+ toast.error('Tagline is required');
+ return;
+ }
+
+ if (!formData.about.trim()) {
+ toast.error('About section is required');
+ return;
+ }
+
+ setIsSaving(true);
+
+ try {
+ if (!updateOrganization || !createOrganization) {
+ throw new Error(
+ 'Organization functions not available. Please refresh the page.'
+ );
+ }
+
+ if (activeOrgId && activeOrg) {
+ await updateOrganization(activeOrgId, {
+ name: formData.name,
+ logo: formData.logo,
+ tagline: formData.tagline,
+ about: formData.about,
+ });
+ toast.success('Organization profile updated successfully');
+ } else {
+ await createOrganization({
+ name: formData.name,
+ logo: formData.logo,
+ tagline: formData.tagline,
+ about: formData.about,
+ });
+ toast.success('Organization created successfully');
+ }
+
+ setHasUserChanges(false);
+
+ if (onSave) {
+ onSave(formData);
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ if (
+ error.message.includes('401') ||
+ error.message.includes('Unauthorized')
+ ) {
+ toast.error('Authentication failed. Please login again.');
+ } else if (
+ error.message.includes('403') ||
+ error.message.includes('Forbidden')
+ ) {
+ toast.error(
+ 'You do not have permission to update this organization.'
+ );
+ } else if (
+ error.message.includes('404') ||
+ error.message.includes('Not Found')
+ ) {
+ toast.error('Organization not found.');
+ } else if (
+ error.message.includes('Network') ||
+ error.message.includes('timeout')
+ ) {
+ toast.error(
+ 'Network error. Please check your connection and try again.'
+ );
+ } else {
+ toast.error(`Failed to save organization profile: ${error.message}`);
+ }
+ } else {
+ toast.error('Failed to save organization profile. Please try again.');
+ }
+ } finally {
+ setIsSaving(false);
+ }
};
return (
- {/* Organization Name */}
- Project Name *
+ Organization Name *
setFormData({ ...formData, name: e.target.value })}
+ onChange={e => handleInputChange('name', e.target.value)}
placeholder='Enter a name for your organization'
className={cn(
'focus-visible:border-primary border-[#484848] bg-[#1A1A1A] p-4 text-white placeholder:text-[#919191]'
)}
+ value={formData.name}
+ disabled={isSaving}
/>
- {/* Logo Upload */}
Logo *
@@ -56,36 +343,91 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
-
-
-
-
-
- Click or drag to upload
-
-
+ {logoPreview || formData.logo ? (
+
+ {isUploading ? (
+
+ ) : (
+ <>
+
+
+
+
Change
+ >
+ )}
+
+ ) : isDragOver ? (
+
+
+
+
+
+ Drop image here
+
+
+ ) : (
+
+
+
+
+
+ Click or drag to upload
+
+
+ )}
@@ -110,23 +452,30 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
480 x 480 px is
recommended.
+ {uploadError && (
+ {uploadError}
+ )}
- {/* Tagline */}
Tagline *
- 0/300
+
+ {formData.tagline.length}/300
+
- {/* About */}
About *
- {/* Rich Text Editor Toolbar */}
- {/* Markdown Editor */}
handleInputChange('about', value || '')}
textareaProps={{
placeholder:
- "Tell your project's full story...\n\nUse text, images, links, or videos to bring your vision to life. Format freely with headings, lists, and more.",
+ "Tell your organization's full story...\n\nUse text, images, links, or videos to bring your vision to life. Format freely with headings, lists, and more.",
style: {
fontSize: 14,
lineHeight: 1.5,
@@ -173,6 +521,7 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
backgroundColor: '#101010',
fontFamily: 'inherit',
},
+ disabled: isSaving,
}}
style={{
backgroundColor: '#101010',
@@ -184,13 +533,30 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
- {/* Save Button */}
-
- Save Changes
-
+
+ {hasUserChanges && (
+
+
+ You have unsaved changes
+
+ )}
+
+ {isSaving
+ ? 'Saving...'
+ : activeOrgId
+ ? 'Save Changes'
+ : 'Create Organization'}
+
+
);
}
diff --git a/components/profile/ProfileDataClient.tsx b/components/profile/ProfileDataClient.tsx
index 5f324f732..19f184eb1 100644
--- a/components/profile/ProfileDataClient.tsx
+++ b/components/profile/ProfileDataClient.tsx
@@ -36,7 +36,7 @@ export default function ProfileDataClient({
const organizationsData =
user.organizations?.map(org => ({
name: org.name,
- avatarUrl: org.avatar || '/blog1.jpg',
+ avatarUrl: (org as unknown as { logo?: string }).logo || '/blog1.jpg',
})) || [];
return (
diff --git a/components/profile/ProfileOverview.tsx b/components/profile/ProfileOverview.tsx
index f4045b7ef..5564e5a18 100644
--- a/components/profile/ProfileOverview.tsx
+++ b/components/profile/ProfileOverview.tsx
@@ -36,7 +36,7 @@ export default function ProfileOverview({ user }: ProfileOverviewProps) {
const organizationsData: Organization[] =
user.organizations?.map(org => ({
name: org.name,
- avatarUrl: org.avatar || '/blog1.jpg',
+ avatarUrl: (org as unknown as { logo?: string }).logo || '/blog1.jpg',
})) || [];
return (
diff --git a/lib/api/organization.ts b/lib/api/organization.ts
new file mode 100644
index 000000000..68ebb96d5
--- /dev/null
+++ b/lib/api/organization.ts
@@ -0,0 +1,642 @@
+import api from './api';
+import { Logger } from '@/lib/logger';
+import { ApiResponse, ErrorResponse, PaginatedResponse } from './types';
+
+export interface OrganizationLinks {
+ website: string;
+ x: string;
+ github: string;
+ others: string;
+}
+
+export interface Organization {
+ _id: string;
+ name: string;
+ logo: string;
+ tagline: string;
+ about: string;
+ links: OrganizationLinks;
+ members: string[];
+ owner: string;
+ hackathons: string[];
+ grants: string[];
+ isProfileComplete: boolean;
+ pendingInvites: string[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface CreateOrganizationRequest {
+ name: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+ links?: Partial
;
+}
+
+export interface CreateOrganizationResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface GetOrganizationResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface GetOrganizationsResponse
+ extends PaginatedResponse {
+ success: true;
+ data: Organization[];
+ pagination: {
+ currentPage: number;
+ totalPages: number;
+ totalItems: number;
+ itemsPerPage: number;
+ hasNext: boolean;
+ hasPrev: boolean;
+ };
+}
+
+export interface UpdateOrganizationProfileRequest {
+ name?: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+}
+
+export interface UpdateOrganizationLinksRequest {
+ website?: string;
+ x?: string;
+ github?: string;
+ others?: string;
+}
+
+export interface UpdateOrganizationMembersRequest {
+ members: string[];
+}
+
+export interface UpdateOrganizationResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface SendInviteRequest {
+ emails: string[];
+}
+
+export interface SendInviteResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface AcceptInviteResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface UpdateHackathonsRequest {
+ action: 'add' | 'remove';
+ hackathonId: string;
+}
+
+export interface UpdateHackathonsResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface UpdateGrantsRequest {
+ action: 'add' | 'remove';
+ grantId: string;
+}
+
+export interface UpdateGrantsResponse extends ApiResponse {
+ success: true;
+ data: Organization;
+ message: string;
+}
+
+export interface DeleteOrganizationResponse extends ApiResponse {
+ success: true;
+ message: string;
+}
+
+/**
+ * Create a new organization
+ */
+export const createOrganization = async (
+ data: CreateOrganizationRequest
+): Promise => {
+ const res = await api.post('/organizations', data);
+ return res.data;
+};
+
+/**
+ * Get all organizations with pagination and filtering
+ */
+export const getOrganizations = async (
+ page = 1,
+ limit = 10,
+ filters?: {
+ search?: string;
+ isProfileComplete?: boolean;
+ owner?: string;
+ }
+): Promise => {
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ });
+
+ if (filters?.search) {
+ params.append('search', filters.search);
+ }
+
+ if (filters?.isProfileComplete !== undefined) {
+ params.append('isProfileComplete', filters.isProfileComplete.toString());
+ }
+
+ if (filters?.owner) {
+ params.append('owner', filters.owner);
+ }
+
+ const res = await api.get(`/organizations?${params.toString()}`);
+ return res.data;
+};
+
+/**
+ * Get a single organization by ID
+ */
+export const getOrganization = async (
+ organizationId: string
+): Promise => {
+ const res = await api.get(`/organizations/${organizationId}`);
+ return res.data;
+};
+
+/**
+ * Update organization profile (name, logo, tagline, about)
+ */
+export const updateOrganizationProfile = async (
+ organizationId: string,
+ data: UpdateOrganizationProfileRequest
+): Promise => {
+ const res = await api.patch(`/organizations/${organizationId}/profile`, data);
+ const logger = new Logger();
+ logger.info({
+ eventType: 'org.api.update_profile.success',
+ orgId: organizationId,
+ });
+ return res.data;
+};
+
+/**
+ * Update organization links
+ */
+export const updateOrganizationLinks = async (
+ organizationId: string,
+ data: UpdateOrganizationLinksRequest
+): Promise => {
+ const res = await api.patch(`/organizations/${organizationId}/links`, data);
+ return res.data;
+};
+
+/**
+ * Update organization members
+ */
+export const updateOrganizationMembers = async (
+ organizationId: string,
+ data: UpdateOrganizationMembersRequest
+): Promise => {
+ const res = await api.patch(`/organizations/${organizationId}/members`, data);
+ return res.data;
+};
+
+/**
+ * Send an invite to a user email
+ */
+export const sendOrganizationInvite = async (
+ organizationId: string,
+ data: SendInviteRequest
+): Promise => {
+ const res = await api.post(`/organizations/${organizationId}/invite`, data);
+ return res.data;
+};
+
+/**
+ * Accept an organization invite
+ */
+export const acceptOrganizationInvite = async (
+ organizationId: string
+): Promise => {
+ const res = await api.post(`/organizations/${organizationId}/accept-invite`);
+ return res.data;
+};
+
+/**
+ * Update organization hackathons (add or remove)
+ */
+export const updateOrganizationHackathons = async (
+ organizationId: string,
+ data: UpdateHackathonsRequest
+): Promise => {
+ const res = await api.patch(
+ `/organizations/${organizationId}/hackathons`,
+ data
+ );
+ return res.data;
+};
+
+/**
+ * Update organization grants (add or remove)
+ */
+export const updateOrganizationGrants = async (
+ organizationId: string,
+ data: UpdateGrantsRequest
+): Promise => {
+ const res = await api.patch(`/organizations/${organizationId}/grants`, data);
+ return res.data;
+};
+
+/**
+ * Delete an organization
+ */
+export const deleteOrganization = async (
+ organizationId: string
+): Promise => {
+ const res = await api.delete(`/organizations/${organizationId}`);
+ return res.data;
+};
+
+/**
+ * Get organizations by user (organizations where user is a member)
+ */
+export const getUserOrganizations = async (
+ userId?: string
+): Promise => {
+ const url = userId ? `/organizations/user/${userId}` : '/organizations/user';
+ const res = await api.get(url);
+ return res.data;
+};
+
+/**
+ * Get organizations owned by user
+ */
+export const getUserOwnedOrganizations = async (
+ userId?: string
+): Promise => {
+ const url = userId
+ ? `/organizations/owned/${userId}`
+ : '/organizations/owned';
+ const res = await api.get(url);
+ return res.data;
+};
+
+/**
+ * Check if user has pending invites
+ */
+export const getPendingInvites = async (): Promise<{
+ success: boolean;
+ data: Organization[];
+ message: string;
+}> => {
+ const res = await api.get('/organizations/pending-invites');
+ return res.data;
+};
+
+/**
+ * Remove a member from organization
+ */
+export const removeOrganizationMember = async (
+ organizationId: string,
+ memberEmail: string
+): Promise => {
+ const res = await api.delete(
+ `/organizations/${organizationId}/members/${memberEmail}`
+ );
+ return res.data;
+};
+
+/**
+ * Cancel a pending invite
+ */
+export const cancelOrganizationInvite = async (
+ organizationId: string,
+ email: string
+): Promise => {
+ const res = await api.delete(
+ `/organizations/${organizationId}/invites/${email}`
+ );
+ return res.data;
+};
+
+/**
+ * Transfer organization ownership
+ */
+export const transferOrganizationOwnership = async (
+ organizationId: string,
+ newOwnerEmail: string
+): Promise => {
+ const res = await api.patch(
+ `/organizations/${organizationId}/transfer-ownership`,
+ {
+ newOwnerEmail,
+ }
+ );
+ return res.data;
+};
+
+/**
+ * Get organization statistics
+ */
+export const getOrganizationStats = async (
+ organizationId: string
+): Promise<{
+ success: boolean;
+ data: {
+ totalMembers: number;
+ totalHackathons: number;
+ totalGrants: number;
+ pendingInvites: number;
+ isProfileComplete: boolean;
+ completionPercentage: number;
+ missingFields: string[];
+ };
+ message: string;
+}> => {
+ const res = await api.get(`/organizations/${organizationId}/stats`);
+ return res.data;
+};
+
+/**
+ * Search organizations
+ */
+export const searchOrganizations = async (
+ query: string,
+ filters?: {
+ isProfileComplete?: boolean;
+ hasHackathons?: boolean;
+ hasGrants?: boolean;
+ }
+): Promise => {
+ const params = new URLSearchParams({
+ q: query,
+ });
+
+ if (filters?.isProfileComplete !== undefined) {
+ params.append('isProfileComplete', filters.isProfileComplete.toString());
+ }
+
+ if (filters?.hasHackathons !== undefined) {
+ params.append('hasHackathons', filters.hasHackathons.toString());
+ }
+
+ if (filters?.hasGrants !== undefined) {
+ params.append('hasGrants', filters.hasGrants.toString());
+ }
+
+ const res = await api.get(`/organizations/search?${params.toString()}`);
+ return res.data;
+};
+
+/**
+ * Get organizations by hackathon
+ */
+export const getOrganizationsByHackathon = async (
+ hackathonId: string,
+ page = 1,
+ limit = 10
+): Promise => {
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ });
+
+ const res = await api.get(
+ `/organizations/hackathon/${hackathonId}?${params.toString()}`
+ );
+ return res.data;
+};
+
+/**
+ * Get organizations by grant
+ */
+export const getOrganizationsByGrant = async (
+ grantId: string,
+ page = 1,
+ limit = 10
+): Promise => {
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ });
+
+ const res = await api.get(
+ `/organizations/grant/${grantId}?${params.toString()}`
+ );
+ return res.data;
+};
+
+/**
+ * Bulk update organization hackathons
+ */
+export const bulkUpdateOrganizationHackathons = async (
+ organizationId: string,
+ hackathonIds: string[]
+): Promise => {
+ const res = await api.patch(
+ `/organizations/${organizationId}/hackathons/bulk`,
+ {
+ hackathonIds,
+ }
+ );
+ return res.data;
+};
+
+/**
+ * Bulk update organization grants
+ */
+export const bulkUpdateOrganizationGrants = async (
+ organizationId: string,
+ grantIds: string[]
+): Promise => {
+ const res = await api.patch(`/organizations/${organizationId}/grants/bulk`, {
+ grantIds,
+ });
+ return res.data;
+};
+
+/**
+ * Export organization data
+ */
+export const exportOrganizationData = async (
+ organizationId: string,
+ format: 'json' | 'csv' = 'json'
+): Promise<{
+ success: boolean;
+ data: {
+ downloadUrl: string;
+ format: string;
+ expiresAt: string;
+ };
+ message: string;
+}> => {
+ const res = await api.get(
+ `/organizations/${organizationId}/export?format=${format}`
+ );
+ return res.data;
+};
+
+/**
+ * Import organization data
+ */
+export const importOrganizationData = async (
+ organizationId: string,
+ data: {
+ file: File;
+ format: 'json' | 'csv';
+ }
+): Promise<{
+ success: boolean;
+ data: {
+ importedCount: number;
+ errors: string[];
+ };
+ message: string;
+}> => {
+ const formData = new FormData();
+ formData.append('file', data.file);
+ formData.append('format', data.format);
+
+ const res = await api.post(
+ `/organizations/${organizationId}/import`,
+ formData,
+ {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ }
+ );
+ return res.data;
+};
+
+// Error handling utilities
+export const isOrganizationError = (error: unknown): error is ErrorResponse => {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ 'success' in error &&
+ 'statusCode' in error
+ );
+};
+
+export const handleOrganizationError = (error: unknown): never => {
+ if (isOrganizationError(error)) {
+ throw new Error(`${error.message} (${error.statusCode})`);
+ }
+ throw new Error('An unexpected error occurred');
+};
+
+// Type guards for runtime type checking
+export const isOrganization = (obj: unknown): obj is Organization => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ '_id' in obj &&
+ 'name' in obj &&
+ 'members' in obj &&
+ 'owner' in obj &&
+ 'isProfileComplete' in obj
+ );
+};
+
+export const isCreateOrganizationRequest = (
+ obj: unknown
+): obj is CreateOrganizationRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ 'name' in obj &&
+ typeof (obj as Record).name === 'string'
+ );
+};
+
+export const isUpdateOrganizationProfileRequest = (
+ obj: unknown
+): obj is UpdateOrganizationProfileRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ Object.keys(obj).some(key =>
+ ['name', 'logo', 'tagline', 'about'].includes(key)
+ )
+ );
+};
+
+export const isUpdateOrganizationLinksRequest = (
+ obj: unknown
+): obj is UpdateOrganizationLinksRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ Object.keys(obj).some(key =>
+ ['website', 'x', 'github', 'others'].includes(key)
+ )
+ );
+};
+
+export const isUpdateOrganizationMembersRequest = (
+ obj: unknown
+): obj is UpdateOrganizationMembersRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ 'members' in obj &&
+ Array.isArray((obj as Record).members)
+ );
+};
+
+export const isSendInviteRequest = (obj: unknown): obj is SendInviteRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ 'emails' in obj &&
+ Array.isArray((obj as Record).emails)
+ );
+};
+
+export const isUpdateHackathonsRequest = (
+ obj: unknown
+): obj is UpdateHackathonsRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ 'action' in obj &&
+ 'hackathonId' in obj &&
+ ['add', 'remove'].includes(
+ (obj as Record).action as string
+ ) &&
+ typeof (obj as Record).hackathonId === 'string'
+ );
+};
+
+export const isUpdateGrantsRequest = (
+ obj: unknown
+): obj is UpdateGrantsRequest => {
+ return (
+ typeof obj === 'object' &&
+ obj !== null &&
+ 'action' in obj &&
+ 'grantId' in obj &&
+ ['add', 'remove'].includes(
+ (obj as Record).action as string
+ ) &&
+ typeof (obj as Record).grantId === 'string'
+ );
+};
diff --git a/lib/api/types.ts b/lib/api/types.ts
index 67c8c1270..f01f6d466 100644
--- a/lib/api/types.ts
+++ b/lib/api/types.ts
@@ -45,12 +45,28 @@ export interface User {
lastLogin?: string;
[key: string]: unknown;
}
+export interface OrganizationLinks {
+ website: string;
+ x: string; // Twitter/X handle
+ github: string;
+ others: string;
+}
+
export interface Organization {
_id: string;
name: string;
- avatar: string;
- username: string;
- bio: string;
+ logo: string;
+ tagline: string;
+ about: string;
+ links: OrganizationLinks;
+ members: string[]; // Array of user emails
+ owner: string; // Owner email or userId
+ hackathons: string[]; // Array of hackathon IDs
+ grants: string[]; // Array of grant IDs
+ isProfileComplete: boolean;
+ pendingInvites: string[]; // Array of emails invited but not yet accepted
+ createdAt: string;
+ updatedAt: string;
}
// Auth tokens
diff --git a/lib/organization-utils.ts b/lib/organization-utils.ts
new file mode 100644
index 000000000..a6b9b3180
--- /dev/null
+++ b/lib/organization-utils.ts
@@ -0,0 +1,431 @@
+import { Organization, OrganizationLinks } from './api/types';
+
+/**
+ * Organization Profile Completion Utilities
+ *
+ * This module provides utilities for checking and calculating organization profile completion status.
+ * Profile completion is determined by several criteria:
+ * 1. Required fields: name, logo, tagline, about must all be filled
+ * 2. At least one link: website, x, github, or others must be present
+ * 3. Members: There must be at least one member (besides the owner)
+ */
+
+export interface ProfileCompletionStatus {
+ isComplete: boolean;
+ completionPercentage: number;
+ missingFields: string[];
+ completedFields: string[];
+}
+
+export interface ProfileCompletionCriteria {
+ hasRequiredFields: boolean;
+ hasAtLeastOneLink: boolean;
+ hasMembers: boolean;
+}
+
+/**
+ * Check if all required profile fields are filled
+ */
+export const hasRequiredFields = (
+ organization: Partial
+): boolean => {
+ return !!(
+ organization.name &&
+ organization.logo &&
+ organization.tagline &&
+ organization.about
+ );
+};
+
+/**
+ * Check if at least one link is provided
+ */
+export const hasAtLeastOneLink = (
+ links: Partial
+): boolean => {
+ if (!links) return false;
+
+ return Boolean(links.website || links.x || links.github || links.others);
+};
+
+/**
+ * Check if organization has members (excluding owner)
+ */
+export const hasMembers = (organization: Partial): boolean => {
+ if (!organization.members || !Array.isArray(organization.members)) {
+ return false;
+ }
+
+ const nonOwnerMembers = organization.members.filter(
+ member => member !== organization.owner
+ );
+
+ return nonOwnerMembers.length > 0;
+};
+
+/**
+ * Get detailed profile completion criteria
+ */
+export const getProfileCompletionCriteria = (
+ organization: Partial
+): ProfileCompletionCriteria => {
+ return {
+ hasRequiredFields: hasRequiredFields(organization),
+ hasAtLeastOneLink: hasAtLeastOneLink(organization.links || {}),
+ hasMembers: hasMembers(organization),
+ };
+};
+
+/**
+ * Calculate profile completion percentage
+ */
+export const calculateCompletionPercentage = (
+ organization: Partial
+): number => {
+ const criteria = getProfileCompletionCriteria(organization);
+ const totalCriteria = 3;
+ const completedCriteria = Object.values(criteria).filter(Boolean).length;
+
+ return Math.round((completedCriteria / totalCriteria) * 100);
+};
+
+/**
+ * Get list of missing fields for profile completion
+ */
+export const getMissingFields = (
+ organization: Partial
+): string[] => {
+ const missing: string[] = [];
+
+ if (!organization.name) missing.push('name');
+ if (!organization.logo) missing.push('logo');
+ if (!organization.tagline) missing.push('tagline');
+ if (!organization.about) missing.push('about');
+
+ const links: Partial = organization.links || {};
+ const hasAnyLink = Boolean(
+ links.website || links.x || links.github || links.others
+ );
+ if (!hasAnyLink) {
+ missing.push('links (at least one of: website, x, github, others)');
+ }
+
+ if (!hasMembers(organization)) {
+ missing.push('members (at least one member besides owner)');
+ }
+
+ return missing;
+};
+
+/**
+ * Get list of completed fields
+ */
+export const getCompletedFields = (
+ organization: Partial
+): string[] => {
+ const completed: string[] = [];
+
+ if (organization.name) completed.push('name');
+ if (organization.logo) completed.push('logo');
+ if (organization.tagline) completed.push('tagline');
+ if (organization.about) completed.push('about');
+
+ const links: Partial = organization.links || {};
+ if (links.website) completed.push('website link');
+ if (links.x) completed.push('x (Twitter) link');
+ if (links.github) completed.push('github link');
+ if (links.others) completed.push('other links');
+
+ if (hasMembers(organization)) {
+ completed.push('members');
+ }
+
+ return completed;
+};
+
+/**
+ * Check if organization profile is complete
+ */
+export const isProfileComplete = (
+ organization: Partial
+): boolean => {
+ const criteria = getProfileCompletionCriteria(organization);
+ return Object.values(criteria).every(Boolean);
+};
+
+/**
+ * Get comprehensive profile completion status
+ */
+export const getProfileCompletionStatus = (
+ organization: Partial
+): ProfileCompletionStatus => {
+ const isComplete = isProfileComplete(organization);
+ const completionPercentage = calculateCompletionPercentage(organization);
+ const missingFields = getMissingFields(organization);
+ const completedFields = getCompletedFields(organization);
+
+ return {
+ isComplete,
+ completionPercentage,
+ missingFields,
+ completedFields,
+ };
+};
+
+/**
+ * Validate organization data for profile completion
+ */
+export const validateOrganizationForCompletion = (
+ organization: Partial
+): { isValid: boolean; errors: string[] } => {
+ const errors: string[] = [];
+
+ if (!organization.name || organization.name.trim() === '') {
+ errors.push('Organization name is required');
+ }
+
+ if (!organization.logo || organization.logo.trim() === '') {
+ errors.push('Organization logo is required');
+ }
+
+ if (!organization.tagline || organization.tagline.trim() === '') {
+ errors.push('Organization tagline is required');
+ }
+
+ if (!organization.about || organization.about.trim() === '') {
+ errors.push('Organization about section is required');
+ }
+
+ const links: Partial = organization.links || {};
+ const hasAnyLink = Boolean(
+ links.website || links.x || links.github || links.others
+ );
+ if (!hasAnyLink) {
+ errors.push('At least one social link is required');
+ }
+
+ if (!organization.members || !Array.isArray(organization.members)) {
+ errors.push('Organization must have members');
+ } else {
+ const nonOwnerMembers = organization.members.filter(
+ member => member !== organization.owner
+ );
+ if (nonOwnerMembers.length === 0) {
+ errors.push(
+ 'Organization must have at least one member besides the owner'
+ );
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ };
+};
+
+/**
+ * Get profile completion suggestions
+ */
+export const getProfileCompletionSuggestions = (
+ organization: Partial
+): string[] => {
+ const suggestions: string[] = [];
+ const status = getProfileCompletionStatus(organization);
+
+ if (status.missingFields.includes('name')) {
+ suggestions.push('Add a clear, descriptive organization name');
+ }
+
+ if (status.missingFields.includes('logo')) {
+ suggestions.push(
+ 'Upload a professional logo that represents your organization'
+ );
+ }
+
+ if (status.missingFields.includes('tagline')) {
+ suggestions.push(
+ "Write a compelling tagline that describes your organization's mission"
+ );
+ }
+
+ if (status.missingFields.includes('about')) {
+ suggestions.push(
+ "Provide a detailed description of your organization's goals and activities"
+ );
+ }
+
+ if (
+ status.missingFields.includes(
+ 'links (at least one of: website, x, github, others)'
+ )
+ ) {
+ suggestions.push(
+ 'Add at least one social media or website link to help people connect with your organization'
+ );
+ }
+
+ if (
+ status.missingFields.includes('members (at least one member besides owner)')
+ ) {
+ suggestions.push(
+ 'Invite team members to join your organization to complete your profile'
+ );
+ }
+
+ return suggestions;
+};
+
+/**
+ * Calculate organization profile score (0-100)
+ */
+export const calculateProfileScore = (
+ organization: Partial
+): number => {
+ getProfileCompletionStatus(organization);
+ let score = 0;
+
+ if (organization.name) score += 15;
+ if (organization.logo) score += 15;
+ if (organization.tagline) score += 15;
+ if (organization.about) score += 15;
+
+ const links: Partial = organization.links || {};
+ if (links.website) score += 5;
+ if (links.x) score += 5;
+ if (links.github) score += 5;
+ if (links.others) score += 5;
+
+ if (hasMembers(organization)) {
+ score += 20;
+ }
+
+ return Math.min(score, 100);
+};
+
+/**
+ * Get organization profile completeness badge
+ */
+export const getProfileCompletenessBadge = (
+ organization: Partial
+): {
+ level: 'incomplete' | 'partial' | 'complete';
+ label: string;
+ color: string;
+} => {
+ const score = calculateProfileScore(organization);
+
+ if (score >= 100) {
+ return {
+ level: 'complete',
+ label: 'Complete',
+ color: 'green',
+ };
+ } else if (score >= 60) {
+ return {
+ level: 'partial',
+ label: 'Partial',
+ color: 'yellow',
+ };
+ } else {
+ return {
+ level: 'incomplete',
+ label: 'Incomplete',
+ color: 'red',
+ };
+ }
+};
+
+/**
+ * Compare two organizations by profile completeness
+ */
+export const compareOrganizationsByCompleteness = (
+ org1: Partial,
+ org2: Partial
+): number => {
+ const score1 = calculateProfileScore(org1);
+ const score2 = calculateProfileScore(org2);
+
+ return score2 - score1;
+};
+
+/**
+ * Filter organizations by profile completeness
+ */
+export const filterOrganizationsByCompleteness = (
+ organizations: Partial[],
+ completeness: 'all' | 'complete' | 'incomplete' | 'partial'
+): Partial[] => {
+ switch (completeness) {
+ case 'complete':
+ return organizations.filter(org => isProfileComplete(org));
+ case 'incomplete':
+ return organizations.filter(org => !isProfileComplete(org));
+ case 'partial':
+ return organizations.filter(org => {
+ const score = calculateProfileScore(org);
+ return score >= 60 && score < 100;
+ });
+ default:
+ return organizations;
+ }
+};
+
+/**
+ * Get organization profile completion progress
+ */
+export const getProfileCompletionProgress = (
+ organization: Partial
+): {
+ current: number;
+ total: number;
+ percentage: number;
+ steps: Array<{
+ name: string;
+ completed: boolean;
+ required: boolean;
+ }>;
+} => {
+ const steps = [
+ {
+ name: 'Organization Name',
+ completed: !!organization.name,
+ required: true,
+ },
+ {
+ name: 'Organization Logo',
+ completed: !!organization.logo,
+ required: true,
+ },
+ {
+ name: 'Organization Tagline',
+ completed: !!organization.tagline,
+ required: true,
+ },
+ {
+ name: 'About Section',
+ completed: !!organization.about,
+ required: true,
+ },
+ {
+ name: 'Social Links',
+ completed: hasAtLeastOneLink(organization.links || {}),
+ required: true,
+ },
+ {
+ name: 'Team Members',
+ completed: hasMembers(organization),
+ required: true,
+ },
+ ];
+
+ const completed = steps.filter(step => step.completed).length;
+ const total = steps.length;
+ const percentage = Math.round((completed / total) * 100);
+
+ return {
+ current: completed,
+ total,
+ percentage,
+ steps,
+ };
+};
diff --git a/lib/providers/OrganizationProvider.tsx b/lib/providers/OrganizationProvider.tsx
new file mode 100644
index 000000000..71049fd18
--- /dev/null
+++ b/lib/providers/OrganizationProvider.tsx
@@ -0,0 +1,893 @@
+'use client';
+
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useReducer,
+ useCallback,
+ useRef,
+} from 'react';
+import { Logger } from '@/lib/logger';
+const logger = new Logger();
+logger.setMinLevel('info');
+import {
+ OrganizationSummary,
+ OrganizationContextValue,
+ OrganizationProviderProps,
+ OrganizationContextState,
+} from './organization-types';
+import { Organization } from '../api/types';
+import { getMe } from '../api/auth';
+import {
+ getOrganization,
+ createOrganization,
+ updateOrganizationProfile,
+ updateOrganizationLinks,
+ updateOrganizationMembers,
+ sendOrganizationInvite,
+ removeOrganizationMember,
+ updateOrganizationHackathons,
+ updateOrganizationGrants,
+ deleteOrganization,
+} from '../api/organization';
+import { getProfileCompletionStatus as getOrgProfileCompletionStatus } from '../organization-utils';
+
+const OrganizationContext = createContext(
+ undefined
+);
+
+const STORAGE_KEYS = {
+ ACTIVE_ORG_ID: 'boundless_active_org_id',
+ ORGANIZATIONS_CACHE: 'boundless_organizations_cache',
+ LAST_UPDATED: 'boundless_orgs_last_updated',
+} as const;
+
+const CACHE_DURATION = 5 * 60 * 1000;
+
+type OrganizationAction =
+ | {
+ type: 'SET_LOADING';
+ payload: {
+ isLoading: boolean;
+ isLoadingOrganizations?: boolean;
+ isLoadingActiveOrg?: boolean;
+ };
+ }
+ | {
+ type: 'SET_ERROR';
+ payload: {
+ error: string | null;
+ organizationsError?: string | null;
+ activeOrgError?: string | null;
+ };
+ }
+ | { type: 'SET_ORGANIZATIONS'; payload: OrganizationSummary[] }
+ | {
+ type: 'SET_ACTIVE_ORG';
+ payload: { org: Organization | null; orgId: string | null };
+ }
+ | { type: 'UPDATE_ORGANIZATION'; payload: Organization }
+ | { type: 'ADD_ORGANIZATION'; payload: OrganizationSummary }
+ | { type: 'REMOVE_ORGANIZATION'; payload: string }
+ | { type: 'SET_LAST_UPDATED'; payload: number }
+ | { type: 'INCREMENT_REFRESH_COUNT' }
+ | { type: 'RESET' };
+
+const initialState: OrganizationContextState = {
+ activeOrg: null,
+ organizations: [],
+ activeOrgId: null,
+ isLoading: false,
+ isLoadingOrganizations: false,
+ isLoadingActiveOrg: false,
+ error: null,
+ organizationsError: null,
+ activeOrgError: null,
+ lastUpdated: 0,
+ refreshCount: 0,
+};
+
+function organizationReducer(
+ state: OrganizationContextState,
+ action: OrganizationAction
+): OrganizationContextState {
+ switch (action.type) {
+ case 'SET_LOADING':
+ return {
+ ...state,
+ isLoading: action.payload.isLoading,
+ isLoadingOrganizations:
+ action.payload.isLoadingOrganizations ?? state.isLoadingOrganizations,
+ isLoadingActiveOrg:
+ action.payload.isLoadingActiveOrg ?? state.isLoadingActiveOrg,
+ };
+
+ case 'SET_ERROR':
+ return {
+ ...state,
+ error: action.payload.error,
+ organizationsError:
+ action.payload.organizationsError ?? state.organizationsError,
+ activeOrgError: action.payload.activeOrgError ?? state.activeOrgError,
+ };
+
+ case 'SET_ORGANIZATIONS':
+ return {
+ ...state,
+ organizations: action.payload,
+ organizationsError: null,
+ };
+
+ case 'SET_ACTIVE_ORG':
+ return {
+ ...state,
+ activeOrg: action.payload.org,
+ activeOrgId: action.payload.orgId,
+ activeOrgError: null,
+ };
+
+ case 'UPDATE_ORGANIZATION':
+ return {
+ ...state,
+ activeOrg:
+ state.activeOrgId === action.payload._id
+ ? action.payload
+ : state.activeOrg,
+ organizations: state.organizations.map(org =>
+ org._id === action.payload._id
+ ? {
+ ...org,
+ name: action.payload.name,
+ logo: action.payload.logo,
+ isProfileComplete: action.payload.isProfileComplete,
+ }
+ : org
+ ),
+ };
+
+ case 'ADD_ORGANIZATION':
+ return {
+ ...state,
+ organizations: [...state.organizations, action.payload],
+ };
+
+ case 'REMOVE_ORGANIZATION':
+ return {
+ ...state,
+ organizations: state.organizations.filter(
+ org => org._id !== action.payload
+ ),
+ activeOrg:
+ state.activeOrgId === action.payload ? null : state.activeOrg,
+ activeOrgId:
+ state.activeOrgId === action.payload ? null : state.activeOrgId,
+ };
+
+ case 'SET_LAST_UPDATED':
+ return {
+ ...state,
+ lastUpdated: action.payload,
+ };
+
+ case 'INCREMENT_REFRESH_COUNT':
+ return {
+ ...state,
+ refreshCount: state.refreshCount + 1,
+ };
+
+ case 'RESET':
+ return initialState;
+
+ default:
+ return state;
+ }
+}
+
+export function OrganizationProvider({
+ children,
+ initialOrgId,
+ autoRefresh = true,
+ refreshInterval = 30000,
+}: OrganizationProviderProps) {
+ const [state, dispatch] = useReducer(organizationReducer, initialState);
+ const refreshTimeoutRef = useRef(null);
+ const isInitializedRef = useRef(false);
+ const isFetchingOrganizationsRef = useRef(false);
+ const isFetchingActiveOrgRef = useRef(false);
+ const fetchOrganizationsTimeoutRef = useRef(null);
+ const fetchActiveOrgTimeoutRef = useRef(null);
+ const setActiveOrgRef = useRef<((orgId: string) => void) | undefined>(
+ undefined
+ );
+ const refreshOrganizationRef = useRef<(() => Promise) | undefined>(
+ undefined
+ );
+
+ useEffect(() => {
+ const loadCachedData = () => {
+ try {
+ const cachedOrgs = localStorage.getItem(
+ STORAGE_KEYS.ORGANIZATIONS_CACHE
+ );
+ const lastUpdated = localStorage.getItem(STORAGE_KEYS.LAST_UPDATED);
+ const activeOrgId = localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID);
+
+ if (cachedOrgs && lastUpdated) {
+ const cacheAge = Date.now() - parseInt(lastUpdated);
+ if (cacheAge < CACHE_DURATION) {
+ const organizations = JSON.parse(cachedOrgs);
+ dispatch({ type: 'SET_ORGANIZATIONS', payload: organizations });
+
+ if (activeOrgId && !isInitializedRef.current) {
+ setActiveOrgRef.current?.(activeOrgId);
+ }
+ }
+ }
+ } catch (error) {
+ logger.error({ eventType: 'org.cache.load_error', error });
+ }
+ };
+
+ loadCachedData();
+ }, []);
+
+ useEffect(() => {
+ if (autoRefresh && state.activeOrgId) {
+ const startAutoRefresh = () => {
+ if (refreshTimeoutRef.current) {
+ clearTimeout(refreshTimeoutRef.current);
+ }
+
+ refreshTimeoutRef.current = setTimeout(() => {
+ refreshOrganizationRef.current?.();
+ startAutoRefresh();
+ }, refreshInterval);
+ };
+
+ startAutoRefresh();
+ }
+
+ // snapshot refs for cleanup to avoid stale ref warnings
+ const refreshTimeoutSnapshot = refreshTimeoutRef.current;
+ const fetchOrganizationsTimeoutSnapshot =
+ fetchOrganizationsTimeoutRef.current;
+ const fetchActiveOrgTimeoutSnapshot = fetchActiveOrgTimeoutRef.current;
+
+ return () => {
+ if (refreshTimeoutSnapshot) {
+ clearTimeout(refreshTimeoutSnapshot);
+ }
+ if (fetchOrganizationsTimeoutSnapshot) {
+ clearTimeout(fetchOrganizationsTimeoutSnapshot);
+ }
+ if (fetchActiveOrgTimeoutSnapshot) {
+ clearTimeout(fetchActiveOrgTimeoutSnapshot);
+ }
+ };
+ }, [autoRefresh, refreshInterval, state.activeOrgId]);
+
+ const fetchOrganizations = useCallback(async () => {
+ logger.info({ eventType: 'org.fetchOrganizations.called' });
+
+ if (isFetchingOrganizationsRef.current) {
+ logger.warn({
+ eventType: 'org.fetchOrganizations.skipped',
+ reason: 'in_progress',
+ });
+ return;
+ }
+
+ isFetchingOrganizationsRef.current = true;
+ logger.info({ eventType: 'org.fetchOrganizations.start' });
+
+ try {
+ dispatch({
+ type: 'SET_LOADING',
+ payload: { isLoading: false, isLoadingOrganizations: true },
+ });
+ dispatch({
+ type: 'SET_ERROR',
+ payload: { error: null, organizationsError: null },
+ });
+
+ const response = await getMe();
+ logger.info({ eventType: 'org.fetchOrganizations.getMe_success' });
+
+ if (!response || typeof response !== 'object') {
+ throw new Error('Invalid response from getMe API');
+ }
+
+ const organizations = response.organizations || [];
+ logger.info({
+ eventType: 'org.fetchOrganizations.organizations_loaded',
+ count: Array.isArray(organizations) ? organizations.length : 0,
+ });
+
+ if (!Array.isArray(organizations)) {
+ logger.warn({
+ eventType: 'org.fetchOrganizations.invalid_organizations_type',
+ });
+ dispatch({ type: 'SET_ORGANIZATIONS', payload: [] });
+ return;
+ }
+
+ type OrgLike = Partial & { id?: string } & Record<
+ string,
+ unknown
+ >;
+ const orgs = organizations as OrgLike[];
+ const organizationSummaries: OrganizationSummary[] = orgs
+ .filter(org => org && typeof org === 'object' && (org._id || org.id))
+ .map(org => ({
+ _id: (org._id as string) ?? (org.id as string),
+ name: (org.name as string) || 'Unnamed Organization',
+ logo: (org.logo as string) || '',
+ tagline: (org.tagline as string) || '',
+ isProfileComplete: Boolean(org.isProfileComplete),
+ role: org.owner === response.email ? 'owner' : 'member',
+ memberCount: Array.isArray(org.members) ? org.members.length : 0,
+ hackathonCount: Array.isArray(org.hackathons)
+ ? org.hackathons.length
+ : 0,
+ grantCount: Array.isArray(org.grants) ? org.grants.length : 0,
+ createdAt: (org.createdAt as string) || new Date().toISOString(),
+ }));
+
+ logger.info({
+ eventType: 'org.fetchOrganizations.transformed',
+ count: organizationSummaries.length,
+ });
+ dispatch({ type: 'SET_ORGANIZATIONS', payload: organizationSummaries });
+ dispatch({ type: 'SET_LAST_UPDATED', payload: Date.now() });
+
+ localStorage.setItem(
+ STORAGE_KEYS.ORGANIZATIONS_CACHE,
+ JSON.stringify(organizationSummaries)
+ );
+ localStorage.setItem(STORAGE_KEYS.LAST_UPDATED, Date.now().toString());
+ } catch (error) {
+ logger.error({ eventType: 'org.fetchOrganizations.error', error });
+
+ let errorMessage = 'Failed to fetch organizations';
+
+ if (error instanceof Error) {
+ errorMessage = error.message;
+ } else if (typeof error === 'object' && error !== null) {
+ if (
+ 'response' in (error as Record) &&
+ (error as Record).response
+ ) {
+ const apiError = (
+ error as {
+ response?: {
+ status?: number;
+ statusText?: string;
+ data?: { message?: string };
+ };
+ }
+ ).response;
+ if (apiError?.data?.message) {
+ errorMessage = apiError.data.message;
+ } else if (apiError?.status === 401) {
+ errorMessage = 'Authentication required. Please log in again.';
+ } else if (apiError?.status === 403) {
+ errorMessage =
+ 'Access denied. You do not have permission to view organizations.';
+ } else if ((apiError?.status ?? 0) >= 500) {
+ errorMessage = 'Server error. Please try again later.';
+ } else {
+ errorMessage = `API Error: ${apiError?.status} ${apiError?.statusText || 'Unknown error'}`;
+ }
+ } else if ('message' in (error as Record)) {
+ errorMessage =
+ (error as { message?: string }).message || errorMessage;
+ }
+ }
+
+ dispatch({
+ type: 'SET_ERROR',
+ payload: { error: null, organizationsError: errorMessage },
+ });
+
+ if (process.env.NODE_ENV === 'development') {
+ logger.warn({
+ eventType: 'org.fetchOrganizations.dev_help',
+ notes: [
+ 'endpoint missing',
+ 'unauthenticated',
+ 'response changed',
+ 'network issues',
+ ],
+ });
+ }
+ } finally {
+ isFetchingOrganizationsRef.current = false;
+ dispatch({
+ type: 'SET_LOADING',
+ payload: { isLoading: false, isLoadingOrganizations: false },
+ });
+ }
+ }, []);
+
+ const fetchActiveOrganization = useCallback(async (orgId: string) => {
+ if (isFetchingActiveOrgRef.current) {
+ logger.warn({
+ eventType: 'org.fetchActiveOrganization.skipped',
+ reason: 'in_progress',
+ });
+ return;
+ }
+
+ isFetchingActiveOrgRef.current = true;
+
+ try {
+ dispatch({
+ type: 'SET_LOADING',
+ payload: { isLoading: false, isLoadingActiveOrg: true },
+ });
+ dispatch({
+ type: 'SET_ERROR',
+ payload: { error: null, activeOrgError: null },
+ });
+
+ const response = await getOrganization(orgId);
+ const organization = response.data;
+
+ dispatch({
+ type: 'SET_ACTIVE_ORG',
+ payload: { org: organization, orgId },
+ });
+ dispatch({ type: 'SET_LAST_UPDATED', payload: Date.now() });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to fetch organization';
+ dispatch({
+ type: 'SET_ERROR',
+ payload: { error: null, activeOrgError: errorMessage },
+ });
+ logger.error({ eventType: 'org.fetchActiveOrganization.error', error });
+ } finally {
+ isFetchingActiveOrgRef.current = false;
+ dispatch({
+ type: 'SET_LOADING',
+ payload: { isLoading: false, isLoadingActiveOrg: false },
+ });
+ }
+ }, []);
+
+ const setActiveOrg = useCallback(
+ (orgId: string) => {
+ const organization = state.organizations.find(org => org._id === orgId);
+ if (!organization) {
+ logger.info({ eventType: 'org.setActiveOrg.miss_then_fetch', orgId });
+
+ localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, orgId);
+
+ fetchActiveOrganization(orgId);
+ isInitializedRef.current = true;
+ return;
+ }
+
+ localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, orgId);
+
+ fetchActiveOrganization(orgId);
+ isInitializedRef.current = true;
+ },
+ [state.organizations, fetchActiveOrganization]
+ );
+ setActiveOrgRef.current = setActiveOrg;
+
+ const refreshOrganization = useCallback(async () => {
+ if (state.activeOrgId) {
+ await fetchActiveOrganization(state.activeOrgId);
+ dispatch({ type: 'INCREMENT_REFRESH_COUNT' });
+ }
+ }, [state.activeOrgId, fetchActiveOrganization]);
+ refreshOrganizationRef.current = refreshOrganization;
+
+ const debouncedFetchOrganizations = useCallback(() => {
+ if (fetchOrganizationsTimeoutRef.current) {
+ clearTimeout(fetchOrganizationsTimeoutRef.current);
+ }
+
+ fetchOrganizationsTimeoutRef.current = setTimeout(() => {
+ fetchOrganizations();
+ }, 300);
+ }, [fetchOrganizations]);
+
+ const refreshOrganizations = useCallback(async () => {
+ debouncedFetchOrganizations();
+ dispatch({ type: 'INCREMENT_REFRESH_COUNT' });
+ }, [debouncedFetchOrganizations]);
+
+ const refreshAll = useCallback(async () => {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ try {
+ await Promise.all([
+ fetchOrganizations(),
+ state.activeOrgId
+ ? fetchActiveOrganization(state.activeOrgId)
+ : Promise.resolve(),
+ ]);
+ dispatch({ type: 'INCREMENT_REFRESH_COUNT' });
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ }, [fetchOrganizations, fetchActiveOrganization, state.activeOrgId]);
+
+ const createOrg = useCallback(
+ async (data: {
+ name: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+ links?: {
+ website?: string;
+ x?: string;
+ github?: string;
+ others?: string;
+ };
+ }) => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ const response = await createOrganization(data);
+ const newOrg = response.data;
+
+ const orgSummary: OrganizationSummary = {
+ _id: newOrg._id,
+ name: newOrg.name,
+ logo: newOrg.logo,
+ tagline: newOrg.tagline,
+ isProfileComplete: newOrg.isProfileComplete,
+ role: 'owner',
+ memberCount: newOrg.members.length,
+ hackathonCount: newOrg.hackathons.length,
+ grantCount: newOrg.grants.length,
+ createdAt: newOrg.createdAt,
+ };
+
+ dispatch({ type: 'ADD_ORGANIZATION', payload: orgSummary });
+ dispatch({
+ type: 'SET_ACTIVE_ORG',
+ payload: { org: newOrg, orgId: newOrg._id },
+ });
+
+ return newOrg;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to create organization';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ },
+ []
+ );
+
+ const updateOrg = useCallback(
+ async (
+ orgId: string,
+ data: Partial<{
+ name: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+ }>
+ ) => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ const response = await updateOrganizationProfile(orgId, data);
+ const updatedOrg = response.data;
+
+ dispatch({ type: 'UPDATE_ORGANIZATION', payload: updatedOrg });
+
+ return updatedOrg;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to update organization';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ },
+ []
+ );
+
+ const updateOrgLinks = useCallback(
+ async (
+ orgId: string,
+ links: { website?: string; x?: string; github?: string; others?: string }
+ ) => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ const response = await updateOrganizationLinks(orgId, links);
+ const updatedOrg = response.data;
+
+ dispatch({ type: 'UPDATE_ORGANIZATION', payload: updatedOrg });
+
+ return updatedOrg;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to update organization links';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ },
+ []
+ );
+
+ const updateOrgMembers = useCallback(
+ async (orgId: string, members: string[]) => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ const response = await updateOrganizationMembers(orgId, { members });
+ const updatedOrg = response.data;
+ dispatch({ type: 'UPDATE_ORGANIZATION', payload: updatedOrg });
+ return updatedOrg;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to update organization members';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ },
+ []
+ );
+
+ const deleteOrg = useCallback(async (orgId: string) => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ await deleteOrganization(orgId);
+
+ dispatch({ type: 'REMOVE_ORGANIZATION', payload: orgId });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to delete organization';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ }, []);
+
+ const inviteMembers = useCallback(
+ async (orgId: string, emails: string[]) => {
+ try {
+ await sendOrganizationInvite(orgId, { emails });
+ await refreshOrganization();
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to invite members';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ }
+ },
+ [refreshOrganization]
+ );
+
+ const removeMember = useCallback(
+ async (orgId: string, email: string) => {
+ try {
+ await removeOrganizationMember(orgId, email);
+ await refreshOrganization();
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to remove member';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ }
+ },
+ [refreshOrganization]
+ );
+
+ const addHackathon = useCallback(
+ async (orgId: string, hackathonId: string) => {
+ try {
+ await updateOrganizationHackathons(orgId, {
+ action: 'add',
+ hackathonId,
+ });
+ await refreshOrganization();
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to add hackathon';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ }
+ },
+ [refreshOrganization]
+ );
+
+ const removeHackathon = useCallback(
+ async (orgId: string, hackathonId: string) => {
+ try {
+ await updateOrganizationHackathons(orgId, {
+ action: 'remove',
+ hackathonId,
+ });
+ await refreshOrganization();
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to remove hackathon';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ }
+ },
+ [refreshOrganization]
+ );
+
+ const addGrant = useCallback(
+ async (orgId: string, grantId: string) => {
+ try {
+ await updateOrganizationGrants(orgId, { action: 'add', grantId });
+ await refreshOrganization();
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to add grant';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ }
+ },
+ [refreshOrganization]
+ );
+
+ const removeGrant = useCallback(
+ async (orgId: string, grantId: string) => {
+ try {
+ await updateOrganizationGrants(orgId, { action: 'remove', grantId });
+ await refreshOrganization();
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to remove grant';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ }
+ },
+ [refreshOrganization]
+ );
+
+ const getOrganizationById = useCallback(
+ (orgId: string) => {
+ return state.organizations.find(org => org._id === orgId);
+ },
+ [state.organizations]
+ );
+
+ const isOwner = useCallback(
+ (orgId?: string) => {
+ const targetOrgId = orgId || state.activeOrgId;
+ if (!targetOrgId) return false;
+
+ const org = getOrganizationById(targetOrgId);
+ return org?.role === 'owner';
+ },
+ [state.activeOrgId, getOrganizationById]
+ );
+
+ const isMember = useCallback(
+ (orgId?: string) => {
+ const targetOrgId = orgId || state.activeOrgId;
+ if (!targetOrgId) return false;
+
+ const org = getOrganizationById(targetOrgId);
+ return org?.role === 'member' || org?.role === 'owner';
+ },
+ [state.activeOrgId, getOrganizationById]
+ );
+
+ const canManage = useCallback(
+ (orgId?: string) => {
+ const targetOrgId = orgId || state.activeOrgId;
+ if (!targetOrgId) return false;
+
+ const org = getOrganizationById(targetOrgId);
+ return org?.role === 'owner';
+ },
+ [state.activeOrgId, getOrganizationById]
+ );
+
+ const getProfileCompletionStatus = useCallback(
+ (orgId?: string) => {
+ const targetOrgId = orgId || state.activeOrgId;
+ if (!targetOrgId || !state.activeOrg) {
+ return { isComplete: false, percentage: 0, missingFields: [] };
+ }
+
+ const status = getOrgProfileCompletionStatus(state.activeOrg);
+ return {
+ isComplete: status.isComplete,
+ percentage: status.completionPercentage,
+ missingFields: status.missingFields,
+ };
+ },
+ [state.activeOrgId, state.activeOrg]
+ );
+
+ useEffect(() => {
+ if (!isInitializedRef.current) {
+ isInitializedRef.current = true;
+ logger.info({ eventType: 'org.initialize.start' });
+
+ fetchOrganizations()
+ .then(() => {
+ const savedOrgId =
+ localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID) || initialOrgId;
+ if (savedOrgId) {
+ logger.info({
+ eventType: 'org.initialize.set_active_after_fetch',
+ orgId: savedOrgId,
+ });
+ setActiveOrgRef.current?.(savedOrgId);
+ }
+ })
+ .catch(error => {
+ logger.error({ eventType: 'org.initialize.fetch_error', error });
+ const savedOrgId =
+ localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID) || initialOrgId;
+ if (savedOrgId) {
+ setActiveOrgRef.current?.(savedOrgId);
+ }
+ });
+ }
+ }, [fetchOrganizations, initialOrgId]);
+
+ // Context value
+ const contextValue: OrganizationContextValue = {
+ ...state,
+ setActiveOrg,
+ refreshOrganization,
+ refreshOrganizations,
+ refreshAll,
+ createOrganization: createOrg,
+ updateOrganization: updateOrg,
+ updateOrganizationLinks: updateOrgLinks,
+ updateOrganizationMembers: updateOrgMembers,
+ deleteOrganization: deleteOrg,
+ inviteMember: inviteMembers,
+ removeMember,
+ addHackathon,
+ removeHackathon,
+ addGrant,
+ removeGrant,
+ getOrganizationById,
+ isOwner,
+ isMember,
+ canManage,
+ getProfileCompletionStatus,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useOrganization(): OrganizationContextValue {
+ const context = useContext(OrganizationContext);
+ if (context === undefined) {
+ throw new Error(
+ 'useOrganization must be used within an OrganizationProvider'
+ );
+ }
+ return context;
+}
+
+export { OrganizationContext };
diff --git a/lib/providers/index.ts b/lib/providers/index.ts
new file mode 100644
index 000000000..53dd92558
--- /dev/null
+++ b/lib/providers/index.ts
@@ -0,0 +1,104 @@
+// Main exports
+export {
+ OrganizationProvider,
+ useOrganization,
+ OrganizationContext,
+} from './OrganizationProvider';
+
+// Hook exports
+export {
+ useActiveOrganization,
+ useOrganizations,
+ useOrganizationLoading,
+ useOrganizationErrors,
+ useOrganizationManagement,
+ useOrganizationUtils,
+ useOrganizationRefresh,
+ useOrganizationSwitching,
+ useOrganizationStats,
+ useOrganizationProfileCompletion,
+ useOrganizationPermissions,
+ useOrganizationById,
+ default as useOrganizationHook,
+} from './useOrganization';
+
+// Type exports
+export type {
+ OrganizationSummary,
+ UserProfileResponse,
+ OrganizationContextValue,
+ OrganizationProviderProps,
+ OrganizationChangeEvent,
+ OrganizationStats,
+ OrganizationFormData,
+ OrganizationInviteData,
+ OrganizationMember,
+ OrganizationActivity,
+ OrganizationNotificationPreferences,
+ OrganizationSettings,
+ OrganizationSearchFilters,
+ OrganizationPagination,
+ OrganizationApiResponse,
+ OrganizationContextState,
+ OrganizationContextActions,
+} from './organization-types';
+
+// Utility exports
+export {
+ organizationToSummary,
+ isOrganizationComplete,
+ getOrganizationCompletionPercentage,
+ getOrganizationMissingFields,
+ isUserOwner,
+ isUserMember,
+ canUserManage,
+ getUserRole,
+ hasPendingInvites,
+ getOrganizationStats,
+ sortOrganizationsByName,
+ sortOrganizationsByCompletion,
+ sortOrganizationsByMemberCount,
+ filterOrganizationsByCompletion,
+ filterOrganizationsByRole,
+ searchOrganizations,
+ getOrganizationDisplayName,
+ getOrganizationAvatar,
+ getOrganizationDescription,
+ // isOrganizationActive, // deprecated export; use isOrganizationComplete or status helpers
+ getOrganizationAge,
+ isNewOrganization,
+ getOrganizationActivityLevel,
+ formatMemberCount,
+ formatHackathonCount,
+ formatGrantCount,
+ getOrganizationStatusBadge,
+ getOrganizationRoleBadge,
+ validateOrganizationData,
+ sanitizeOrganizationData,
+ getOrganizationStorageKey,
+ getOrganizationCacheKey,
+ isOrganizationDataCached,
+ cacheOrganizationData,
+ getCachedOrganizationData,
+ clearOrganizationCache,
+ getOrganizationBreadcrumb,
+ getOrganizationSlug,
+ isOrganizationNameAvailable,
+ generateOrganizationSuggestions,
+} from './organization-utils';
+
+// Example component exports
+export {
+ OrganizationSwitcher,
+ OrganizationSwitcherWithIcons,
+ OrganizationSwitcherDropdown,
+} from './examples/OrganizationSwitcher';
+
+export { OrganizationDashboard } from './examples/OrganizationDashboard';
+
+export {
+ OrganizationForm,
+ OrganizationInviteForm,
+} from './examples/OrganizationForm';
+
+// ExampleUsage not included; remove exports to avoid type errors
diff --git a/lib/providers/organization-types.ts b/lib/providers/organization-types.ts
new file mode 100644
index 000000000..24585e683
--- /dev/null
+++ b/lib/providers/organization-types.ts
@@ -0,0 +1,188 @@
+import { Organization } from '../api/types';
+
+export interface OrganizationSummary {
+ _id: string;
+ name: string;
+ logo?: string;
+ tagline?: string;
+ isProfileComplete: boolean;
+ role: 'owner' | 'member';
+ memberCount: number;
+ hackathonCount: number;
+ grantCount: number;
+ createdAt: string;
+}
+
+export interface UserProfileResponse {
+ user: {
+ email: string;
+ firstName: string;
+ lastName: string;
+ username: string;
+ avatar?: string;
+ organizations: OrganizationSummary[];
+ };
+}
+
+export type OrganizationContextValue = OrganizationContextState &
+ OrganizationContextActions;
+
+export interface OrganizationProviderProps {
+ children: React.ReactNode;
+ initialOrgId?: string;
+ autoRefresh?: boolean;
+ refreshInterval?: number;
+}
+
+export interface OrganizationChangeEvent {
+ previousOrgId: string | null;
+ newOrgId: string;
+ organization: Organization;
+}
+
+export interface OrganizationStats {
+ totalMembers: number;
+ totalHackathons: number;
+ totalGrants: number;
+ pendingInvites: number;
+ isProfileComplete: boolean;
+ completionPercentage: number;
+ missingFields: string[];
+}
+
+export interface OrganizationFormData {
+ name: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+ links?: {
+ website?: string;
+ x?: string;
+ github?: string;
+ others?: string;
+ };
+}
+
+export interface OrganizationInviteData {
+ email: string;
+ role?: 'member' | 'admin';
+ message?: string;
+}
+
+export interface OrganizationMember {
+ email: string;
+ role: 'owner' | 'admin' | 'member';
+ joinedAt: string;
+ isActive: boolean;
+ avatar?: string;
+ name?: string;
+}
+
+export interface OrganizationActivity {
+ id: string;
+ type:
+ | 'member_joined'
+ | 'member_left'
+ | 'hackathon_added'
+ | 'grant_added'
+ | 'profile_updated';
+ description: string;
+ timestamp: string;
+ user?: {
+ name: string;
+ email: string;
+ avatar?: string;
+ };
+}
+
+export interface OrganizationNotificationPreferences {
+ emailInvites: boolean;
+ emailUpdates: boolean;
+ emailHackathons: boolean;
+ emailGrants: boolean;
+ pushNotifications: boolean;
+}
+
+export interface OrganizationSettings {
+ allowPublicView: boolean;
+ allowMemberInvites: boolean;
+ requireApprovalForJoining: boolean;
+ defaultMemberRole: 'member' | 'admin';
+ notificationPreferences: OrganizationNotificationPreferences;
+}
+
+export interface OrganizationSearchFilters {
+ search?: string;
+ isProfileComplete?: boolean;
+ hasHackathons?: boolean;
+ hasGrants?: boolean;
+ role?: 'owner' | 'member' | 'all';
+ sortBy?: 'name' | 'createdAt' | 'memberCount' | 'completion';
+ sortOrder?: 'asc' | 'desc';
+}
+
+export interface OrganizationPagination {
+ page: number;
+ limit: number;
+ total: number;
+ totalPages: number;
+ hasNext: boolean;
+ hasPrev: boolean;
+}
+
+export interface OrganizationApiResponse {
+ success: boolean;
+ data: T;
+ message: string;
+ timestamp: string;
+}
+
+export interface OrganizationContextState {
+ activeOrg: Organization | null;
+ organizations: OrganizationSummary[];
+ activeOrgId: string | null;
+ isLoading: boolean;
+ isLoadingOrganizations: boolean;
+ isLoadingActiveOrg: boolean;
+ error: string | null;
+ organizationsError: string | null;
+ activeOrgError: string | null;
+ lastUpdated: number;
+ refreshCount: number;
+}
+
+export interface OrganizationContextActions {
+ setActiveOrg: (orgId: string) => void;
+ refreshOrganization: () => Promise;
+ refreshOrganizations: () => Promise;
+ refreshAll: () => Promise;
+ createOrganization: (data: OrganizationFormData) => Promise;
+ updateOrganization: (
+ orgId: string,
+ data: Partial
+ ) => Promise;
+ updateOrganizationLinks: (
+ orgId: string,
+ links: { website?: string; x?: string; github?: string; others?: string }
+ ) => Promise;
+ updateOrganizationMembers: (
+ orgId: string,
+ members: string[]
+ ) => Promise;
+ deleteOrganization: (orgId: string) => Promise;
+ removeMember: (orgId: string, email: string) => Promise;
+ inviteMember: (orgId: string, emails: string[]) => Promise;
+ addHackathon: (orgId: string, hackathonId: string) => Promise;
+ removeHackathon: (orgId: string, hackathonId: string) => Promise;
+ addGrant: (orgId: string, grantId: string) => Promise;
+ removeGrant: (orgId: string, grantId: string) => Promise;
+ getOrganizationById: (orgId: string) => OrganizationSummary | undefined;
+ isOwner: (orgId?: string) => boolean;
+ isMember: (orgId?: string) => boolean;
+ canManage: (orgId?: string) => boolean;
+ getProfileCompletionStatus: (orgId?: string) => {
+ isComplete: boolean;
+ percentage: number;
+ missingFields: string[];
+ };
+}
diff --git a/lib/providers/organization-utils.ts b/lib/providers/organization-utils.ts
new file mode 100644
index 000000000..c6830031f
--- /dev/null
+++ b/lib/providers/organization-utils.ts
@@ -0,0 +1,518 @@
+import { Organization } from '../api/types';
+import { OrganizationSummary } from './organization-types';
+import { getProfileCompletionStatus } from '../organization-utils';
+
+/**
+ * Organization utility functions for the provider
+ */
+
+/**
+ * Convert full organization to summary format
+ */
+export function organizationToSummary(
+ org: Organization,
+ userRole: 'owner' | 'member' = 'member'
+): OrganizationSummary {
+ return {
+ _id: org._id,
+ name: org.name,
+ logo: org.logo,
+ tagline: org.tagline,
+ isProfileComplete: org.isProfileComplete,
+ role: userRole,
+ memberCount: org.members.length,
+ hackathonCount: org.hackathons.length,
+ grantCount: org.grants.length,
+ createdAt: org.createdAt,
+ };
+}
+
+/**
+ * Check if organization is complete
+ */
+export function isOrganizationComplete(org: Organization): boolean {
+ return getProfileCompletionStatus(org).isComplete;
+}
+
+/**
+ * Get organization completion percentage
+ */
+export function getOrganizationCompletionPercentage(org: Organization): number {
+ return getProfileCompletionStatus(org).completionPercentage;
+}
+
+/**
+ * Get organization missing fields
+ */
+export function getOrganizationMissingFields(org: Organization): string[] {
+ return getProfileCompletionStatus(org).missingFields;
+}
+
+/**
+ * Check if user is owner of organization
+ */
+export function isUserOwner(org: Organization, userEmail: string): boolean {
+ return org.owner === userEmail;
+}
+
+/**
+ * Check if user is member of organization
+ */
+export function isUserMember(org: Organization, userEmail: string): boolean {
+ return org.members.includes(userEmail) || org.owner === userEmail;
+}
+
+/**
+ * Check if user can manage organization
+ */
+export function canUserManage(org: Organization, userEmail: string): boolean {
+ return org.owner === userEmail;
+}
+
+/**
+ * Get user role in organization
+ */
+export function getUserRole(
+ org: Organization,
+ userEmail: string
+): 'owner' | 'member' | null {
+ if (org.owner === userEmail) return 'owner';
+ if (org.members.includes(userEmail)) return 'member';
+ return null;
+}
+
+/**
+ * Check if organization has pending invites
+ */
+export function hasPendingInvites(org: Organization): boolean {
+ return org.pendingInvites.length > 0;
+}
+
+/**
+ * Get organization statistics
+ */
+export function getOrganizationStats(org: Organization) {
+ return {
+ totalMembers: org.members.length,
+ totalHackathons: org.hackathons.length,
+ totalGrants: org.grants.length,
+ pendingInvites: org.pendingInvites.length,
+ isProfileComplete: org.isProfileComplete,
+ completionPercentage: getOrganizationCompletionPercentage(org),
+ missingFields: getOrganizationMissingFields(org),
+ };
+}
+
+/**
+ * Sort organizations by name
+ */
+export function sortOrganizationsByName(
+ orgs: OrganizationSummary[]
+): OrganizationSummary[] {
+ return [...orgs].sort((a, b) => a.name.localeCompare(b.name));
+}
+
+/**
+ * Sort organizations by completion status
+ */
+export function sortOrganizationsByCompletion(
+ orgs: OrganizationSummary[]
+): OrganizationSummary[] {
+ return [...orgs].sort((a, b) => {
+ if (a.isProfileComplete === b.isProfileComplete) {
+ return a.name.localeCompare(b.name);
+ }
+ return a.isProfileComplete ? -1 : 1;
+ });
+}
+
+/**
+ * Sort organizations by member count
+ */
+export function sortOrganizationsByMemberCount(
+ orgs: OrganizationSummary[]
+): OrganizationSummary[] {
+ return [...orgs].sort((a, b) => b.memberCount - a.memberCount);
+}
+
+/**
+ * Filter organizations by completion status
+ */
+export function filterOrganizationsByCompletion(
+ orgs: OrganizationSummary[],
+ isComplete: boolean
+): OrganizationSummary[] {
+ return orgs.filter(org => org.isProfileComplete === isComplete);
+}
+
+/**
+ * Filter organizations by role
+ */
+export function filterOrganizationsByRole(
+ orgs: OrganizationSummary[],
+ role: 'owner' | 'member'
+): OrganizationSummary[] {
+ return orgs.filter(org => org.role === role);
+}
+
+/**
+ * Search organizations by name or tagline
+ */
+export function searchOrganizations(
+ orgs: OrganizationSummary[],
+ query: string
+): OrganizationSummary[] {
+ const lowercaseQuery = query.toLowerCase();
+ return orgs.filter(
+ org =>
+ org.name.toLowerCase().includes(lowercaseQuery) ||
+ (org.tagline && org.tagline.toLowerCase().includes(lowercaseQuery))
+ );
+}
+
+/**
+ * Get organization display name
+ */
+export function getOrganizationDisplayName(
+ org: OrganizationSummary | Organization
+): string {
+ return org.name;
+}
+
+/**
+ * Get organization avatar URL
+ */
+export function getOrganizationAvatar(
+ org: OrganizationSummary | Organization
+): string | null {
+ return org.logo || null;
+}
+
+/**
+ * Get organization description
+ */
+export function getOrganizationDescription(
+ org: OrganizationSummary | Organization
+): string {
+ if ('about' in org) {
+ return org.about || org.tagline || '';
+ }
+ return org.tagline || '';
+}
+
+/**
+ * Check if organization is active
+ */
+// export function isOrganizationActive(org: Organization): boolean {
+// // Add any business logic for determining if org is active
+// return true; // For now, all orgs are considered active
+// }
+
+/**
+ * Get organization age in days
+ */
+export function getOrganizationAge(org: Organization): number {
+ const createdAt = new Date(org.createdAt);
+ const now = new Date();
+ const diffTime = Math.abs(now.getTime() - createdAt.getTime());
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+}
+
+/**
+ * Check if organization is new (less than 30 days old)
+ */
+export function isNewOrganization(org: Organization): boolean {
+ return getOrganizationAge(org) < 30;
+}
+
+/**
+ * Get organization activity level based on member count and age
+ */
+export function getOrganizationActivityLevel(
+ org: Organization
+): 'low' | 'medium' | 'high' {
+ const memberCount = org.members.length;
+ const age = getOrganizationAge(org);
+
+ if (memberCount >= 10 && age >= 30) return 'high';
+ if (memberCount >= 5 || age >= 14) return 'medium';
+ return 'low';
+}
+
+/**
+ * Format organization member count
+ */
+export function formatMemberCount(count: number): string {
+ if (count === 0) return 'No members';
+ if (count === 1) return '1 member';
+ return `${count} members`;
+}
+
+/**
+ * Format organization hackathon count
+ */
+export function formatHackathonCount(count: number): string {
+ if (count === 0) return 'No hackathons';
+ if (count === 1) return '1 hackathon';
+ return `${count} hackathons`;
+}
+
+/**
+ * Format organization grant count
+ */
+export function formatGrantCount(count: number): string {
+ if (count === 0) return 'No grants';
+ if (count === 1) return '1 grant';
+ return `${count} grants`;
+}
+
+/**
+ * Get organization status badge
+ */
+export function getOrganizationStatusBadge(
+ org: OrganizationSummary | Organization
+) {
+ if (org.isProfileComplete) {
+ return { label: 'Complete', color: 'green', variant: 'success' };
+ }
+
+ const completionPercentage =
+ 'members' in org && 'owner' in org
+ ? getOrganizationCompletionPercentage(org as Organization)
+ : 0;
+
+ if (completionPercentage >= 75) {
+ return { label: 'Almost Complete', color: 'yellow', variant: 'warning' };
+ }
+
+ return { label: 'Incomplete', color: 'red', variant: 'error' };
+}
+
+/**
+ * Get organization role badge
+ */
+export function getOrganizationRoleBadge(role: 'owner' | 'member') {
+ switch (role) {
+ case 'owner':
+ return { label: 'Owner', color: 'purple', variant: 'primary' };
+ case 'member':
+ return { label: 'Member', color: 'blue', variant: 'secondary' };
+ default:
+ return { label: 'Unknown', color: 'gray', variant: 'default' };
+ }
+}
+
+/**
+ * Validate organization data
+ */
+export interface OrganizationBasicData {
+ name?: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+ links?: Partial<{
+ website: string;
+ x: string;
+ github: string;
+ others: string;
+ }>;
+}
+
+export function validateOrganizationData(data: OrganizationBasicData): {
+ isValid: boolean;
+ errors: string[];
+} {
+ const errors: string[] = [];
+
+ if (!data.name || data.name.trim() === '') {
+ errors.push('Organization name is required');
+ }
+
+ if (data.name && data.name.length > 100) {
+ errors.push('Organization name must be less than 100 characters');
+ }
+
+ if (data.tagline && data.tagline.length > 200) {
+ errors.push('Tagline must be less than 200 characters');
+ }
+
+ if (data.about && data.about.length > 1000) {
+ errors.push('About section must be less than 1000 characters');
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ };
+}
+
+/**
+ * Sanitize organization data
+ */
+export function sanitizeOrganizationData(
+ data: OrganizationBasicData
+): OrganizationBasicData {
+ return {
+ name: data.name?.trim(),
+ logo: data.logo?.trim(),
+ tagline: data.tagline?.trim(),
+ about: data.about?.trim(),
+ links: {
+ website: data.links?.website?.trim(),
+ x: data.links?.x?.trim(),
+ github: data.links?.github?.trim(),
+ others: data.links?.others?.trim(),
+ },
+ };
+}
+
+/**
+ * Get organization storage key
+ */
+export function getOrganizationStorageKey(orgId: string): string {
+ return `boundless_org_${orgId}`;
+}
+
+/**
+ * Get organization cache key
+ */
+export function getOrganizationCacheKey(orgId: string): string {
+ return `org_${orgId}_cache`;
+}
+
+/**
+ * Check if organization data is cached
+ */
+export function isOrganizationDataCached(orgId: string): boolean {
+ const cacheKey = getOrganizationCacheKey(orgId);
+ const cached = localStorage.getItem(cacheKey);
+
+ if (!cached) return false;
+
+ try {
+ const data = JSON.parse(cached);
+ const cacheAge = Date.now() - data.timestamp;
+ return cacheAge < 5 * 60 * 1000; // 5 minutes
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Cache organization data
+ */
+export function cacheOrganizationData(orgId: string, data: Organization): void {
+ const cacheKey = getOrganizationCacheKey(orgId);
+ const cacheData = {
+ data,
+ timestamp: Date.now(),
+ };
+
+ try {
+ localStorage.setItem(cacheKey, JSON.stringify(cacheData));
+ } catch {}
+}
+
+/**
+ * Get cached organization data
+ */
+export function getCachedOrganizationData(orgId: string): Organization | null {
+ const cacheKey = getOrganizationCacheKey(orgId);
+ const cached = localStorage.getItem(cacheKey);
+
+ if (!cached) return null;
+
+ try {
+ const data = JSON.parse(cached);
+ return data.data;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Clear organization cache
+ */
+export function clearOrganizationCache(orgId?: string): void {
+ if (orgId) {
+ const cacheKey = getOrganizationCacheKey(orgId);
+ localStorage.removeItem(cacheKey);
+ } else {
+ // Clear all organization caches
+ const keys = Object.keys(localStorage);
+ keys.forEach(key => {
+ if (key.startsWith('org_') && key.endsWith('_cache')) {
+ localStorage.removeItem(key);
+ }
+ });
+ }
+}
+
+/**
+ * Get organization breadcrumb
+ */
+export function getOrganizationBreadcrumb(
+ org: OrganizationSummary | Organization
+): string[] {
+ return ['Organizations', org.name];
+}
+
+/**
+ * Get organization URL slug
+ */
+export function getOrganizationSlug(
+ org: OrganizationSummary | Organization
+): string {
+ return org.name
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+}
+
+/**
+ * Check if organization name is available
+ */
+export function isOrganizationNameAvailable(
+ name: string,
+ existingOrgs: OrganizationSummary[]
+): boolean {
+ const normalizedName = name.toLowerCase().trim();
+ return !existingOrgs.some(
+ org => org.name.toLowerCase().trim() === normalizedName
+ );
+}
+
+/**
+ * Generate organization suggestions
+ */
+export function generateOrganizationSuggestions(org: Organization): string[] {
+ const suggestions: string[] = [];
+
+ if (!org.logo) {
+ suggestions.push('Add a logo to make your organization more recognizable');
+ }
+
+ if (!org.tagline) {
+ suggestions.push("Add a tagline to describe your organization's mission");
+ }
+
+ if (!org.about) {
+ suggestions.push(
+ 'Add an about section to tell people more about your organization'
+ );
+ }
+
+ const hasLinks =
+ org.links.website || org.links.x || org.links.github || org.links.others;
+ if (!hasLinks) {
+ suggestions.push(
+ 'Add social media links to help people connect with your organization'
+ );
+ }
+
+ if (org.members.length <= 1) {
+ suggestions.push('Invite team members to join your organization');
+ }
+
+ return suggestions;
+}
diff --git a/lib/providers/useOrganization.ts b/lib/providers/useOrganization.ts
new file mode 100644
index 000000000..1b21f2434
--- /dev/null
+++ b/lib/providers/useOrganization.ts
@@ -0,0 +1,417 @@
+import { useContext } from 'react';
+import { OrganizationContext } from './OrganizationProvider';
+import { OrganizationContextValue } from './organization-types';
+
+/**
+ * Custom hook to access organization context
+ *
+ * @returns OrganizationContextValue - The organization context value
+ * @throws Error if used outside of OrganizationProvider
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { activeOrg, organizations, setActiveOrg } = useOrganization();
+ *
+ * return (
+ *
+ *
{activeOrg?.name}
+ * setActiveOrg(e.target.value)}>
+ * {organizations.map(org => (
+ * {org.name}
+ * ))}
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganization(): OrganizationContextValue {
+ const context = useContext(OrganizationContext);
+
+ if (context === undefined) {
+ throw new Error(
+ 'useOrganization must be used within an OrganizationProvider'
+ );
+ }
+
+ return context;
+}
+
+/**
+ * Hook to get only the active organization
+ *
+ * @returns The active organization or null
+ *
+ * @example
+ * ```tsx
+ * function OrganizationHeader() {
+ * const activeOrg = useActiveOrganization();
+ *
+ * if (!activeOrg) return null;
+ *
+ * return {activeOrg.name} ;
+ * }
+ * ```
+ */
+export function useActiveOrganization() {
+ const { activeOrg } = useOrganization();
+ return activeOrg;
+}
+
+/**
+ * Hook to get only the organizations list
+ *
+ * @returns Array of organization summaries
+ *
+ * @example
+ * ```tsx
+ * function OrganizationList() {
+ * const organizations = useOrganizations();
+ *
+ * return (
+ *
+ * {organizations.map(org => (
+ * {org.name}
+ * ))}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganizations() {
+ const { organizations } = useOrganization();
+ return organizations;
+}
+
+/**
+ * Hook to get organization loading states
+ *
+ * @returns Object with loading states
+ *
+ * @example
+ * ```tsx
+ * function OrganizationLoader() {
+ * const { isLoading, isLoadingOrganizations, isLoadingActiveOrg } = useOrganizationLoading();
+ *
+ * if (isLoading) return ;
+ * if (isLoadingOrganizations) return Loading organizations... ;
+ * if (isLoadingActiveOrg) return Loading organization details... ;
+ *
+ * return null;
+ * }
+ * ```
+ */
+export function useOrganizationLoading() {
+ const { isLoading, isLoadingOrganizations, isLoadingActiveOrg } =
+ useOrganization();
+ return { isLoading, isLoadingOrganizations, isLoadingActiveOrg };
+}
+
+/**
+ * Hook to get organization error states
+ *
+ * @returns Object with error states
+ *
+ * @example
+ * ```tsx
+ * function OrganizationError() {
+ * const { error, organizationsError, activeOrgError } = useOrganizationErrors();
+ *
+ * if (error) return {error} ;
+ * if (organizationsError) return {organizationsError} ;
+ * if (activeOrgError) return {activeOrgError} ;
+ *
+ * return null;
+ * }
+ * ```
+ */
+export function useOrganizationErrors() {
+ const { error, organizationsError, activeOrgError } = useOrganization();
+ return { error, organizationsError, activeOrgError };
+}
+
+/**
+ * Hook to get organization management functions
+ *
+ * @returns Object with management functions
+ *
+ * @example
+ * ```tsx
+ * function OrganizationManager() {
+ * const {
+ * createOrganization,
+ * updateOrganization,
+ * deleteOrganization,
+ * inviteMember,
+ * removeMember
+ * } = useOrganizationManagement();
+ *
+ * const handleCreate = async (data) => {
+ * try {
+ * await createOrganization(data);
+ * toast.success('Organization created!');
+ * } catch (error) {
+ * toast.error('Failed to create organization');
+ * }
+ * };
+ *
+ * return handleCreate({ name: 'New Org' })}>Create ;
+ * }
+ * ```
+ */
+export function useOrganizationManagement() {
+ const {
+ createOrganization,
+ updateOrganization,
+ deleteOrganization,
+ inviteMember,
+ removeMember,
+ addHackathon,
+ removeHackathon,
+ addGrant,
+ removeGrant,
+ } = useOrganization();
+
+ return {
+ createOrganization,
+ updateOrganization,
+ deleteOrganization,
+ inviteMember,
+ removeMember,
+ addHackathon,
+ removeHackathon,
+ addGrant,
+ removeGrant,
+ };
+}
+
+/**
+ * Hook to get organization utility functions
+ *
+ * @returns Object with utility functions
+ *
+ * @example
+ * ```tsx
+ * function OrganizationUtils() {
+ * const {
+ * isOwner,
+ * isMember,
+ * canManage,
+ * getProfileCompletionStatus
+ * } = useOrganizationUtils();
+ *
+ * const canEdit = canManage();
+ * const profileStatus = getProfileCompletionStatus();
+ *
+ * return (
+ *
+ * {canEdit &&
}
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganizationUtils() {
+ const {
+ isOwner,
+ isMember,
+ canManage,
+ getProfileCompletionStatus,
+ getOrganizationById,
+ } = useOrganization();
+
+ return {
+ isOwner,
+ isMember,
+ canManage,
+ getProfileCompletionStatus,
+ getOrganizationById,
+ };
+}
+
+/**
+ * Hook to get organization refresh functions
+ *
+ * @returns Object with refresh functions
+ *
+ * @example
+ * ```tsx
+ * function RefreshButton() {
+ * const { refreshOrganization, refreshOrganizations, refreshAll } = useOrganizationRefresh();
+ *
+ * return (
+ *
+ * Refresh Current
+ Refresh List
+ Refresh All
+
+ );
+ * }
+ * ```
+ */
+export function useOrganizationRefresh() {
+ const { refreshOrganization, refreshOrganizations, refreshAll } =
+ useOrganization();
+ return { refreshOrganization, refreshOrganizations, refreshAll };
+}
+
+/**
+ * Hook to get organization switching functions
+ *
+ * @returns Object with switching functions
+ *
+ * @example
+ * ```tsx
+ * function OrganizationSwitcher() {
+ * const { activeOrgId, organizations, setActiveOrg } = useOrganizationSwitching();
+ *
+ * return (
+ * setActiveOrg(e.target.value)}>
+ * {organizations.map(org => (
+ * {org.name}
+ * ))}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganizationSwitching() {
+ const { activeOrgId, organizations, setActiveOrg } = useOrganization();
+ return { activeOrgId, organizations, setActiveOrg };
+}
+
+/**
+ * Hook to get organization statistics
+ *
+ * @returns Object with organization statistics
+ *
+ * @example
+ * ```tsx
+ * function OrganizationStats() {
+ * const { activeOrg } = useOrganization();
+ *
+ * if (!activeOrg) return null;
+ *
+ * return (
+ *
+ *
Members: {activeOrg.members.length}
+ *
Hackathons: {activeOrg.hackathons.length}
+ *
Grants: {activeOrg.grants.length}
+ *
Profile Complete: {activeOrg.isProfileComplete ? 'Yes' : 'No'}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganizationStats() {
+ const { activeOrg } = useOrganization();
+
+ if (!activeOrg) {
+ return {
+ memberCount: 0,
+ hackathonCount: 0,
+ grantCount: 0,
+ pendingInviteCount: 0,
+ isProfileComplete: false,
+ };
+ }
+
+ return {
+ memberCount: activeOrg.members.length,
+ hackathonCount: activeOrg.hackathons.length,
+ grantCount: activeOrg.grants.length,
+ pendingInviteCount: activeOrg.pendingInvites.length,
+ isProfileComplete: activeOrg.isProfileComplete,
+ };
+}
+
+/**
+ * Hook to get organization profile completion status
+ *
+ * @returns Object with profile completion information
+ *
+ * @example
+ * ```tsx
+ * function ProfileCompletion() {
+ * const { isComplete, percentage, missingFields } = useOrganizationProfileCompletion();
+ *
+ * return (
+ *
+ *
+ * {!isComplete && (
+ *
+ * {missingFields.map(field => (
+ * {field}
+ * ))}
+ *
+ * )}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganizationProfileCompletion() {
+ const { getProfileCompletionStatus } = useOrganization();
+ return getProfileCompletionStatus();
+}
+
+/**
+ * Hook to check if user has specific permissions
+ *
+ * @param orgId - Optional organization ID (defaults to active org)
+ * @returns Object with permission checks
+ *
+ * @example
+ * ```tsx
+ * function PermissionGate({ children, requiredPermission }) {
+ * const { isOwner, isMember, canManage } = useOrganizationPermissions();
+ *
+ * if (requiredPermission === 'owner' && !isOwner()) return null;
+ * if (requiredPermission === 'member' && !isMember()) return null;
+ * if (requiredPermission === 'manage' && !canManage()) return null;
+ *
+ * return children;
+ * }
+ * ```
+ */
+export function useOrganizationPermissions(orgId?: string) {
+ const { isOwner, isMember, canManage } = useOrganization();
+
+ return {
+ isOwner: () => isOwner(orgId),
+ isMember: () => isMember(orgId),
+ canManage: () => canManage(orgId),
+ };
+}
+
+/**
+ * Hook to get organization by ID
+ *
+ * @param orgId - Organization ID
+ * @returns Organization summary or undefined
+ *
+ * @example
+ * ```tsx
+ * function OrganizationCard({ orgId }) {
+ * const org = useOrganizationById(orgId);
+ *
+ * if (!org) return Organization not found
;
+ *
+ * return (
+ *
+ *
{org.name}
+ *
{org.tagline}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useOrganizationById(orgId: string) {
+ const { getOrganizationById } = useOrganization();
+ return getOrganizationById(orgId);
+}
+
+// Re-export the main hook as default
+export default useOrganization;