Skip to content
20 changes: 9 additions & 11 deletions app/(landing)/organizations/[id]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,23 @@

import { useParams } from 'next/navigation';
import OrganizationSettings from '@/components/organization/OrganizationSettings';
import { useEffect } from 'react';
import { useOrganization } from '@/lib/providers';

export default function OrganizationSettingsPage() {
const params = useParams();
const organizationId = params.id as string;
const { setActiveOrg } = useOrganization();

const mockOrgData = {
name: 'Boundless',
logo: '/tech-company-logo.jpg',
tagline: 'Building the future of technology',
about: 'We are a community of innovators...',
};

useEffect(() => {
if (organizationId) {
setActiveOrg(organizationId);
}
}, [organizationId, setActiveOrg]);
return (
<div className='min-h-screen bg-black text-white'>
<div className='flex'>
<OrganizationSettings
organizationId={organizationId}
initialData={mockOrgData}
/>
<OrganizationSettings organizationId={organizationId} />
</div>
</div>
);
Expand Down
11 changes: 3 additions & 8 deletions app/(landing)/organizations/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@ import OrganizationSettings from '@/components/organization/OrganizationSettings

export default function NewOrganizationPage() {
return (
<OrganizationSettings
initialData={{
name: 'Organization 1',
logo: '',
tagline: '',
about: '',
}}
/>
<div className='min-h-screen bg-black'>
<OrganizationSettings isCreating={true} />
</div>
);
}
2 changes: 1 addition & 1 deletion components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ const SignupForm = ({ onLoadingChange, invitation }: SignupFormProps) => {
<LockIcon className='h-5 w-5 flex-shrink-0 text-[#B5B5B5]' />
<Input
{...field}
type='text'
type='password'
placeholder='Enter your password'
className='w-full border-none bg-transparent text-white caret-white placeholder:text-[#B5B5B5] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
Expand Down
21 changes: 18 additions & 3 deletions components/organization/OrganizationHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ export default function OrganizationHeader() {
const { isLoading, user } = useAuthStatus();
const { logout } = useAuthActions();
const pathname = usePathname();
const isOnOrganizationsPage = pathname === '/dashboard/organizations';
const isOnOrganizationsPage = pathname === '/organizations';
const { organizations, activeOrg, setActiveOrg } = useOrganization();

// Show organization selector on org pages, but NOT on /organizations/new
const showOrgSelector =
pathname !== '/organizations' && pathname.startsWith('/organizations');
pathname !== '/organizations' &&
pathname !== '/organizations/new' &&
pathname.startsWith('/organizations') &&
organizations.length > 0;

return (
<header className='sticky top-0 z-50 flex w-full flex-wrap items-center justify-between overflow-x-hidden border-b border-b-zinc-800 bg-black px-4 py-3 md:flex-nowrap md:px-10'>
Expand All @@ -44,7 +49,17 @@ export default function OrganizationHeader() {
</button>
</Link>

{showOrgSelector && organizations && organizations.length > 0 && (
{/* Show title for new organization page */}
{pathname === '/organizations/new' && (
<div className='flex items-center gap-2'>
<span className='text-xs font-medium text-gray-700'>
/ Create New Organization
</span>
</div>
)}

{/* Show organization selector on other org pages */}
{showOrgSelector && (
<OrganizationSelector
organizations={organizations}
currentOrganization={
Expand Down
105 changes: 63 additions & 42 deletions components/organization/OrganizationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LinksTab from './tabs/LinksTab';
import MembersTab from './tabs/MembersTab';
import TransferOwnershipTab from './tabs/TransferOwnershipTab';
import { ScrollArea, ScrollBar } from '../ui/scroll-area';

interface OrganizationSettingsProps {
organizationId?: string;
initialData?: {
Expand All @@ -17,11 +18,13 @@ interface OrganizationSettingsProps {
tagline?: string;
about?: string;
};
isCreating?: boolean;
}

export default function OrganizationSettings({
organizationId,
initialData,
isCreating = false,
}: OrganizationSettingsProps) {
const [modalOpen, setModalOpen] = useState(false);

Expand Down Expand Up @@ -49,15 +52,18 @@ export default function OrganizationSettings({
<Tabs defaultValue='profile' className='w-full'>
<div className='border-b border-zinc-800 px-6 md:px-20'>
<div className='flex items-center gap-4'>
{/* Hamburger Menu - Visible only on medium screens and below */}
<button
onClick={() => setModalOpen(true)}
className='rounded-lg p-2 transition-colors hover:bg-zinc-800 md:hidden'
aria-label='Open menu'
>
<Menu className='h-5 w-5' />
</button>
<div className='h-[50px] w-[0.5px] bg-gray-900 md:hidden' />
{!isCreating && (
<>
<button
onClick={() => setModalOpen(true)}
className='rounded-lg p-2 transition-colors hover:bg-zinc-800 md:hidden'
aria-label='Open menu'
>
<Menu className='h-5 w-5' />
</button>
<div className='h-[50px] w-[0.5px] bg-gray-900 md:hidden' />
</>
)}

<ScrollArea className='w-full'>
<div className='flex w-max min-w-full'>
Expand All @@ -68,24 +74,28 @@ export default function OrganizationSettings({
>
Profile
</TabsTrigger>
<TabsTrigger
value='links'
className='data-[state=active]:border-b-primary rounded-none border-b-2 border-transparent bg-transparent px-0 pt-4 pb-3 text-sm font-medium text-zinc-400 transition-all data-[state=active]:text-white data-[state=active]:shadow-none'
>
Links
</TabsTrigger>
<TabsTrigger
value='members'
className='data-[state=active]:border-b-primary rounded-none border-b-2 border-transparent bg-transparent px-0 pt-4 pb-3 text-sm font-medium text-zinc-400 transition-all data-[state=active]:text-white data-[state=active]:shadow-none'
>
Members
</TabsTrigger>
<TabsTrigger
value='transfer'
className='data-[state=active]:border-b-primary rounded-none border-b-2 border-transparent bg-transparent px-0 pt-4 pb-3 text-sm font-medium text-zinc-400 transition-all data-[state=active]:text-white data-[state=active]:shadow-none'
>
Transfer Ownership
</TabsTrigger>
{!isCreating && (
<>
<TabsTrigger
value='links'
className='data-[state=active]:border-b-primary rounded-none border-b-2 border-transparent bg-transparent px-0 pt-4 pb-3 text-sm font-medium text-zinc-400 transition-all data-[state=active]:text-white data-[state=active]:shadow-none'
>
Links
</TabsTrigger>
<TabsTrigger
value='members'
className='data-[state=active]:border-b-primary rounded-none border-b-2 border-transparent bg-transparent px-0 pt-4 pb-3 text-sm font-medium text-zinc-400 transition-all data-[state=active]:text-white data-[state=active]:shadow-none'
>
Members
</TabsTrigger>
<TabsTrigger
value='transfer'
className='data-[state=active]:border-b-primary rounded-none border-b-2 border-transparent bg-transparent px-0 pt-4 pb-3 text-sm font-medium text-zinc-400 transition-all data-[state=active]:text-white data-[state=active]:shadow-none'
>
Transfer Ownership
</TabsTrigger>
</>
)}
</TabsList>
</div>
<ScrollBar orientation='horizontal' className='h-px' />
Expand All @@ -95,28 +105,39 @@ export default function OrganizationSettings({

<div className='px-6 py-6 md:px-20'>
<TabsContent value='profile' className='mt-0'>
<ProfileTab initialData={initialData} onSave={handleProfileSave} />
<ProfileTab
organizationId={organizationId}
initialData={initialData}
onSave={handleProfileSave}
isCreating={isCreating}
/>
</TabsContent>

<TabsContent value='links' className='mt-0'>
<LinksTab onSave={handleLinksSave} />
</TabsContent>
{!isCreating && (
<>
<TabsContent value='links' className='mt-0'>
<LinksTab onSave={handleLinksSave} />
</TabsContent>

<TabsContent value='members' className='mt-0'>
<MembersTab onSave={handleMembersSave} />
</TabsContent>
<TabsContent value='members' className='mt-0'>
<MembersTab onSave={handleMembersSave} />
</TabsContent>

<TabsContent value='transfer' className='mt-0'>
<TransferOwnershipTab onTransfer={handleTransferOwnership} />
</TabsContent>
<TabsContent value='transfer' className='mt-0'>
<TransferOwnershipTab onTransfer={handleTransferOwnership} />
</TabsContent>
</>
)}
</div>
</Tabs>

<ResponsiveSidebar
organizationId={organizationId}
open={modalOpen}
onOpenChange={setModalOpen}
/>
{!isCreating && (
<ResponsiveSidebar
organizationId={organizationId}
open={modalOpen}
onOpenChange={setModalOpen}
/>
)}
</div>
);
}
26 changes: 17 additions & 9 deletions components/organization/cards/OrganizationSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,24 @@ export default function OrganizationSelector({
const currentPath = pathname;
const orgId = org._id;

if (currentPath.startsWith('/organizations/')) {
const pathAfterOrgs = currentPath.replace('/organizations/', '');
// Don't navigate if we're on the /organizations/new page
if (currentPath === '/organizations/new') {
return;
}

if (pathAfterOrgs.match(/^[a-f0-9]{24}/)) {
const newPath = currentPath.replace(
/^\/organizations\/[a-f0-9]{24}/,
`/organizations/${orgId}`
);
if (currentPath.startsWith('/organizations/')) {
const pathSegments = currentPath.split('/').filter(Boolean);

// Check if second segment is an org ID (MongoDB ObjectId pattern or UUID)
if (pathSegments[1] && pathSegments[1].match(/^[a-f0-9]{24}$/i)) {
// Replace the org ID in the path
const pathAfterOrgId = pathSegments.slice(2).join('/');
const newPath = pathAfterOrgId
? `/organizations/${orgId}/${pathAfterOrgId}`
: `/organizations/${orgId}`;
router.push(newPath);
} else {
// If no org ID in path, just navigate to the org
router.push(`/organizations/${orgId}`);
}
} else if (currentPath === '/organizations') {
Expand Down Expand Up @@ -108,7 +116,7 @@ export default function OrganizationSelector({
<Button className='flex items-center gap-3 bg-transparent px-3 py-2 transition-colors hover:bg-transparent focus:ring-0 focus-visible:ring-0'>
<div className='relative h-10 w-10 overflow-hidden rounded-full bg-white'>
<Image
src={selectedOrg.logo || ''}
src={selectedOrg.logo || '/placeholder-org.png'}
alt={selectedOrg.name || 'Organization'}
fill
className='object-cover'
Expand Down Expand Up @@ -137,7 +145,7 @@ export default function OrganizationSelector({
>
<div className='relative h-8 w-8 flex-shrink-0 overflow-hidden rounded-full bg-white'>
<Image
src={org.logo || ''}
src={org.logo || '/placeholder-org.png'}
alt={org.name}
fill
className='object-cover'
Expand Down
15 changes: 14 additions & 1 deletion components/organization/tabs/LinksTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,15 @@ export default function LinksTab({
);
}

await updateOrganizationLinks(activeOrgId, links);
// Convert empty strings to undefined - only validate non-empty fields
const linksToSend = {
website: links.website.trim() || undefined,
x: links.x.trim() || undefined,
github: links.github.trim() || undefined,
others: links.others.trim() || undefined,
};

await updateOrganizationLinks(activeOrgId, linksToSend);

toast.success('Organization links updated successfully');
setHasUserChanges(false);
Expand Down Expand Up @@ -123,6 +131,11 @@ export default function LinksTab({
toast.error(
'Network error. Please check your connection and try again.'
);
} else if (
error.message.includes('Validation failed') ||
error.message.includes('valid URL')
) {
toast.error('Please enter valid URLs for all links.');
} else {
toast.error(`Failed to save organization links: ${error.message}`);
}
Expand Down
22 changes: 17 additions & 5 deletions components/organization/tabs/MembersTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useOrganization } from '@/lib/providers/OrganizationProvider';
import EmailInviteSection from './MembersTab/EmailInviteSection';
import PermissionsTable from './MembersTab/PermissionsTable';
import TeamManagementSection from './MembersTab/TeamManagementSection';
import { toast } from 'sonner';

interface Member {
id: string;
Expand All @@ -28,6 +29,7 @@ export default function MembersTab({ onSave }: MembersTabProps) {
updateOrganizationMembers,
inviteMember,
removeMember,
assignRole,
isLoading,
} = useOrganization();

Expand All @@ -54,12 +56,21 @@ export default function MembersTab({ onSave }: MembersTabProps) {
}
};

const handleRoleChange = (memberId: string) => {
const handleRoleChange = async (memberId: string, newRole: string) => {
if (!activeOrgId) return;
const m = members.find(x => x.id === memberId);
if (!m) return;
updateOrganizationMembers(activeOrgId, [m.email]);
setHasUserChanges(true);
const member = members.find(m => m.id === memberId);
if (!member) return;

try {
const action = newRole === 'admin' ? 'promote' : 'demote';
await assignRole(activeOrgId, member.email, action);
setHasUserChanges(true);
toast.success(`Member role updated to ${newRole}`);
} catch (error) {
// Handle error (show toast, etc.)
const msg = error instanceof Error ? error.message : String(error);
toast.error(`Failed to update member role: ${msg}`);
}
};

const handleRemoveMember = (memberId: string) => {
Expand Down Expand Up @@ -95,6 +106,7 @@ export default function MembersTab({ onSave }: MembersTabProps) {
members={members}
onRoleChange={handleRoleChange}
onRemoveMember={handleRemoveMember}
activeOrg={activeOrg}
/>

<div className='space-y-2'>
Expand Down
Loading
Loading