-
-
-
- {ownerMember.name.charAt(0).toUpperCase()}
-
-
-
-
{ownerMember.name}
-
{ownerMember.email}
-
- Owner
-
+ {orgOwner && (
+
+
+
+
+ {orgOwner.name.charAt(0).toUpperCase()}
+
+
+
+
{orgOwner.name}
+
{orgOwner.email}
+
+ Owner
+
+
-
+ )}
- {activeMembers.filter(member => member.role !== 'owner').length > 0 && (
+ {activeMembers.filter(member => member.email !== activeOrg?.owner)
+ .length > 0 && (
{activeMembers
- .filter(member => member.role !== 'owner')
+ .filter(member => member.email !== activeOrg?.owner)
.map(member => (
) => void;
+ isCreating?: boolean;
}
-export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
+export default function ProfileTab({
+ organizationId,
+ initialData,
+ onSave,
+ isCreating = false,
+}: ProfileTabProps) {
const {
activeOrgId,
activeOrg,
@@ -48,9 +56,22 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef(null);
const previousOrgIdRef = useRef(null);
+ const router = useRouter();
useEffect(() => {
- if (!isInitialized) {
+ if (isCreating) {
+ // For new organization, start with empty form
+ setFormData({
+ name: '',
+ logo: '',
+ tagline: '',
+ about: '',
+ });
+ setLogoPreview('');
+ setHasUserChanges(false);
+ setIsInitialized(true);
+ } else if (!isInitialized) {
+ // For existing organization, load from activeOrg or initialData
if (activeOrg) {
setFormData({
name: activeOrg.name || '',
@@ -74,10 +95,10 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
setIsInitialized(true);
}
}
- }, [activeOrg, initialData, isInitialized]);
+ }, [activeOrg, initialData, isInitialized, isCreating]);
useEffect(() => {
- if (activeOrg && isInitialized) {
+ if (activeOrg && isInitialized && !isCreating) {
const currentOrgId = activeOrg._id;
const previousOrgId = previousOrgIdRef.current;
@@ -93,7 +114,7 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
previousOrgIdRef.current = currentOrgId;
}
}
- }, [activeOrg, isInitialized]);
+ }, [activeOrg, isInitialized, isCreating]);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -240,6 +261,11 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
return;
}
+ if (!formData.logo.trim()) {
+ toast.error('Logo is required');
+ return;
+ }
+
if (!formData.tagline.trim()) {
toast.error('Tagline is required');
return;
@@ -259,25 +285,34 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
);
}
- if (activeOrgId && activeOrg) {
- await updateOrganization(activeOrgId, {
+ if (isCreating) {
+ // POST request - create new organization
+ const newOrg = await createOrganization({
name: formData.name,
logo: formData.logo,
tagline: formData.tagline,
about: formData.about,
});
- toast.success('Organization profile updated successfully');
- } else {
- await createOrganization({
+
+ toast.success('Organization created successfully');
+
+ // Redirect to the edit page
+ setTimeout(() => {
+ router.push(`/organizations/${newOrg._id}/settings`);
+ }, 500);
+ } else if (organizationId || activeOrgId) {
+ // PATCH request - update existing organization
+ const orgId = organizationId || activeOrgId;
+ await updateOrganization(orgId as string, {
name: formData.name,
logo: formData.logo,
tagline: formData.tagline,
about: formData.about,
});
- toast.success('Organization created successfully');
- }
- setHasUserChanges(false);
+ toast.success('Organization profile updated successfully');
+ setHasUserChanges(false);
+ }
if (onSave) {
onSave(formData);
@@ -530,7 +565,7 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
- {hasUserChanges && (
+ {hasUserChanges && !isCreating && (
You have unsaved changes
@@ -547,10 +582,12 @@ export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
)}
>
{isSaving
- ? 'Saving...'
- : activeOrgId
- ? 'Save Changes'
- : 'Create Organization'}
+ ? isCreating
+ ? 'Creating...'
+ : 'Saving...'
+ : isCreating
+ ? 'Create Organization'
+ : 'Save Changes'}
diff --git a/components/organization/tabs/TransferOwnershipTab.tsx b/components/organization/tabs/TransferOwnershipTab.tsx
index 1d72578b7..ef254ffcb 100644
--- a/components/organization/tabs/TransferOwnershipTab.tsx
+++ b/components/organization/tabs/TransferOwnershipTab.tsx
@@ -12,33 +12,42 @@ import {
import { Check, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { BoundlessButton } from '@/components/buttons';
+import { useOrganization } from '@/lib/providers/OrganizationProvider';
+import { toast } from 'sonner';
interface Member {
id: string;
name: string;
email: string;
avatar?: string;
- role: 'admin' | 'member';
+ role: 'member';
}
interface TransferOwnershipTabProps {
- currentOwner?: {
- name: string;
- email: string;
- avatar?: string;
- };
- members?: Member[];
onTransfer?: (newOwnerId: string) => void;
}
export default function TransferOwnershipTab({
- members = [],
onTransfer,
}: TransferOwnershipTabProps) {
const [selectedMember, setSelectedMember] = useState
('');
const [isTransferring, setIsTransferring] = useState(false);
const [popoverWidth, setPopoverWidth] = useState(0);
const triggerRef = useRef(null);
+ const { activeOrg, activeOrgId, transferOwnership } = useOrganization();
+
+ // Create members list using the SAME logic as MembersTab
+ const members: Member[] =
+ activeOrg?.members
+ ?.filter(email => email !== activeOrg?.owner) // Exclude owner
+ ?.map((email, idx) => ({
+ id: `${idx}-${email}`,
+ name: email.split('@')[0] || email,
+ email,
+ role: 'member' as const,
+ joinedAt: new Date().toISOString(),
+ status: 'active' as const,
+ })) || [];
useEffect(() => {
const updateWidth = () => {
@@ -52,50 +61,41 @@ export default function TransferOwnershipTab({
return () => window.removeEventListener('resize', updateWidth);
}, []);
- // Dummy data for demonstration
- const dummyMembers: Member[] = [
- {
- id: 'admin-1',
- name: 'Robert Fox',
- email: 'robert.fox@example.com',
- avatar: '/avatar.png',
- role: 'admin',
- },
- {
- id: 'admin-2',
- name: 'Sarah Johnson',
- email: 'sarah.johnson@example.com',
- avatar: '/avatar.png',
- role: 'admin',
- },
- {
- id: 'admin-3',
- name: 'Mike Chen',
- email: 'mike.chen@example.com',
- avatar: '/avatar.png',
- role: 'admin',
- },
- ];
-
- const eligibleMembers =
- members.length > 0
- ? members.filter(member => member.role === 'admin')
- : dummyMembers;
- const selectedMemberData = eligibleMembers.find(
+
+ const selectedMemberData = members.find(
member => member.id === selectedMember
);
const handleTransfer = async () => {
- if (!selectedMember) return;
+ if (!selectedMember || !activeOrgId) return;
setIsTransferring(true);
try {
- await onTransfer?.(selectedMember);
+ const selectedMemberData = members.find(m => m.id === selectedMember);
+ if (!selectedMemberData) {
+ toast.error('Selected member not found');
+ return;
+ }
+
+ await transferOwnership(activeOrgId, selectedMemberData.email);
+
+ toast.success('Ownership transferred successfully');
+
+ onTransfer?.(selectedMember);
+ setSelectedMember(''); // Reset selection
+ } catch (error) {
+ toast.error(
+ error instanceof Error ? error.message : 'Failed to transfer ownership'
+ );
} finally {
setIsTransferring(false);
}
};
+ // Debug: Log the members to see what's happening
+ console.log('TransferOwnershipTab - activeOrg:', activeOrg);
+ console.log('TransferOwnershipTab - members:', members);
+
return (
<>
@@ -111,6 +111,10 @@ export default function TransferOwnershipTab({
you will be demoted to admin.
+ {/* Debug info */}
+
+ Available members: {members.length}
+
@@ -148,41 +152,52 @@ export default function TransferOwnershipTab({
className='bg-background w-full border-gray-900'
style={{ width: popoverWidth > 0 ? `${popoverWidth}px` : '100%' }}
>
- {eligibleMembers.map(member => (
-
setSelectedMember(member.id)}
- >
-
-
-
- {member.name.charAt(0).toUpperCase()}
-
-
-
-
{member.name}
-
{member.email}
-
- {selectedMember === member.id && (
-
- )}
-
- ))}
+ {members.length > 0 ? (
+ members.map(member => (
+
setSelectedMember(member.id)}
+ >
+
+
+
+ {member.name.charAt(0).toUpperCase()}
+
+
+
+
{member.name}
+
+ {member.email}
+
+
+ {selectedMember === member.id && (
+
+ )}
+
+ ))
+ ) : (
+
+ No members found (only owner in organization)
+
+ )}
>
);
diff --git a/components/project-details/project-voters/Empty.tsx b/components/project-details/project-voters/Empty.tsx
index 483297f3f..6a2fe7531 100644
--- a/components/project-details/project-voters/Empty.tsx
+++ b/components/project-details/project-voters/Empty.tsx
@@ -4,6 +4,7 @@ import { ArrowUpIcon } from 'lucide-react';
import LottieAnimation from '@/components/LottieAnimation';
interface EmptyProps {
+ projectStatus: string;
onVote?: () => void;
}
diff --git a/components/project-details/project-voters/index.tsx b/components/project-details/project-voters/index.tsx
index eb239db62..260ed79dd 100644
--- a/components/project-details/project-voters/index.tsx
+++ b/components/project-details/project-voters/index.tsx
@@ -86,9 +86,8 @@ const ProjectVoters = ({ project }: ProjectVotersProps) => {
// Could implement navigation to user profile page
window.open(`/profile/${voter.profile.username}`, '_blank');
};
-
if (voters.length === 0) {
- return
diff --git a/lib/api/api.ts b/lib/api/api.ts
index 1af831c4d..05df9201f 100644
--- a/lib/api/api.ts
+++ b/lib/api/api.ts
@@ -2,7 +2,8 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import Cookies from 'js-cookie';
import { useAuthStore } from '@/lib/stores/auth-store';
-const API_BASE_URL = 'https://staging-api.boundlessfi.xyz/api';
+// const API_BASE_URL = 'https://staging-api.boundlessfi.xyz/api';
+const API_BASE_URL = 'http://localhost:8000/api';
// const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
if (!API_BASE_URL) {
throw new Error('NEXT_PUBLIC_API_URL environment variable is not defined');
diff --git a/lib/api/organization.ts b/lib/api/organization.ts
index 68ebb96d5..e4604b052 100644
--- a/lib/api/organization.ts
+++ b/lib/api/organization.ts
@@ -26,6 +26,11 @@ export interface Organization {
updatedAt: string;
}
+export interface AssignRoleRequest {
+ action: 'promote' | 'demote';
+ email: string;
+}
+
export interface CreateOrganizationRequest {
name: string;
logo?: string;
@@ -127,6 +132,12 @@ export interface DeleteOrganizationResponse extends ApiResponse {
message: string;
}
+export interface AssignRoleResponse extends ApiResponse
{
+ success: true;
+ data: Organization;
+ message: string;
+}
+
/**
* Create a new organization
*/
@@ -526,6 +537,14 @@ export const importOrganizationData = async (
return res.data;
};
+export const assignOrganizationRole = async (
+ organizationId: string,
+ data: AssignRoleRequest
+): Promise => {
+ const res = await api.patch(`/organizations/${organizationId}/roles`, data);
+ return res.data;
+};
+
// Error handling utilities
export const isOrganizationError = (error: unknown): error is ErrorResponse => {
return (
diff --git a/lib/providers/OrganizationProvider.tsx b/lib/providers/OrganizationProvider.tsx
index c55934020..cc201a0fb 100644
--- a/lib/providers/OrganizationProvider.tsx
+++ b/lib/providers/OrganizationProvider.tsx
@@ -30,6 +30,8 @@ import {
updateOrganizationHackathons,
updateOrganizationGrants,
deleteOrganization,
+ assignOrganizationRole,
+ transferOrganizationOwnership,
} from '../api/organization';
import { getProfileCompletionStatus as getOrgProfileCompletionStatus } from '../organization-utils';
@@ -828,7 +830,7 @@ export function OrganizationProvider({
fetchOrganizations()
.then(() => {
const savedOrgId =
- localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID) || initialOrgId;
+ initialOrgId || localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID);
if (savedOrgId) {
logger.info({
eventType: 'org.initialize.set_active_after_fetch',
@@ -848,6 +850,50 @@ export function OrganizationProvider({
}
}, [fetchOrganizations, initialOrgId]);
+ const assignRole = useCallback(
+ async (orgId: string, email: string, action: 'promote' | 'demote') => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ const response = await assignOrganizationRole(orgId, { action, email });
+ const updatedOrg = response.data;
+ dispatch({ type: 'UPDATE_ORGANIZATION', payload: updatedOrg });
+ return updatedOrg;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to assign role';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ },
+ []
+ );
+
+ const transferOwnership = useCallback(
+ async (orgId: string, newOwnerEmail: string) => {
+ try {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: true } });
+ const response = await transferOrganizationOwnership(
+ orgId,
+ newOwnerEmail
+ );
+ const updatedOrg = response.data;
+ dispatch({ type: 'UPDATE_ORGANIZATION', payload: updatedOrg });
+ return updatedOrg;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : 'Failed to transfer ownership';
+ dispatch({ type: 'SET_ERROR', payload: { error: errorMessage } });
+ throw error;
+ } finally {
+ dispatch({ type: 'SET_LOADING', payload: { isLoading: false } });
+ }
+ },
+ []
+ );
// Context value
const contextValue: OrganizationContextValue = {
...state,
@@ -871,6 +917,8 @@ export function OrganizationProvider({
isMember,
canManage,
getProfileCompletionStatus,
+ assignRole,
+ transferOwnership,
};
return (
diff --git a/lib/providers/organization-types.ts b/lib/providers/organization-types.ts
index 84ceef752..589be6988 100644
--- a/lib/providers/organization-types.ts
+++ b/lib/providers/organization-types.ts
@@ -185,4 +185,13 @@ export interface OrganizationContextActions {
percentage: number;
missingFields: string[];
};
+ assignRole: (
+ orgId: string,
+ email: string,
+ action: 'promote' | 'demote'
+ ) => Promise;
+ transferOwnership: (
+ orgId: string,
+ newOwnerEmail: string
+ ) => Promise;
}