From c9b3ac9ef9beb1ea831c215308323c2f6c96eecc Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Sat, 5 Jul 2025 11:47:19 +0300 Subject: [PATCH 1/5] Searching authors not working in notebook --- .../components/AuthorsSection.tsx | 183 ++++++++++++++---- .../components/ContactsSection.tsx | 177 ++++++++++++++--- .../components/PublishingForm/index.tsx | 44 ++++- 3 files changed, 343 insertions(+), 61 deletions(-) diff --git a/app/notebook/components/PublishingForm/components/AuthorsSection.tsx b/app/notebook/components/PublishingForm/components/AuthorsSection.tsx index b23bae6bc..6b7ed57a4 100644 --- a/app/notebook/components/PublishingForm/components/AuthorsSection.tsx +++ b/app/notebook/components/PublishingForm/components/AuthorsSection.tsx @@ -1,14 +1,13 @@ -import { Users } from 'lucide-react'; +import { Users, X } from 'lucide-react'; import { SectionHeader } from './SectionHeader'; -import { - SearchableMultiSelect, - MultiSelectOption, -} from '@/components/ui/form/SearchableMultiSelect'; -import { useEffect, useState } from 'react'; +import { AutocompleteSelect, SelectOption } from '@/components/ui/form/AutocompleteSelect'; import { useFormContext } from 'react-hook-form'; import { useNotebookContext } from '@/contexts/NotebookContext'; import { getFieldErrorMessage } from '@/utils/form'; import { useUser } from '@/contexts/UserContext'; +import { SearchService } from '@/services/search.service'; +import { AuthorSuggestion } from '@/types/search'; +import { Button } from '@/components/ui/Button'; export function AuthorsSection() { const { @@ -17,49 +16,161 @@ export function AuthorsSection() { formState: { errors }, } = useFormContext(); - const { users, isLoadingUsers, currentNote } = useNotebookContext(); + const { isLoadingUsers } = useNotebookContext(); const { user: currentUser } = useUser(); - const [authorOptions, setAuthorOptions] = useState([]); const authors = watch('authors') || []; - useEffect(() => { - if (users?.users) { - const options = users.users - .filter((user) => user.name || user.email) - .map((user) => ({ - value: user.authorId.toString(), - label: user.name || user.email || 'Unknown User', - })); + // Search function for AutocompleteSelect + const handleSearchAuthors = async (query: string): Promise[]> => { + if (!query.trim()) return []; - setAuthorOptions(options); + try { + const results: AuthorSuggestion[] = await SearchService.suggestPeople(query); + return results.map((author) => ({ + value: author.id?.toString() || author.fullName || Date.now().toString(), + label: author.fullName || 'Unnamed Author', + data: author, + })); + } catch (error) { + console.error('Error searching authors:', error); + return []; + } + }; - // Auto-add admin if no authors are selected yet - if (authors.length === 0) { - const admin = users.users.find((user) => user.id === currentUser?.id.toString()); + // Handle author selection + const handleAuthorSelect = (selectedOption: SelectOption | null) => { + if (selectedOption) { + // Check if author is already selected + const isAlreadySelected = authors.some( + (author: any) => author.value === selectedOption.value + ); - if (admin) { - const adminOption = { - value: admin.authorId.toString(), - label: admin.name || admin.email || 'Unknown User', - }; - setValue('authors', [adminOption], { shouldValidate: true }); - } + if (!isAlreadySelected) { + const newAuthors = [...authors, selectedOption]; + setValue('authors', newAuthors, { shouldValidate: true }); } } - }, [users, setValue, currentNote]); + }; + + // Handle author removal + const handleRemoveAuthor = (authorToRemove: SelectOption) => { + const newAuthors = authors.filter((author: any) => author.value !== authorToRemove.value); + setValue('authors', newAuthors, { shouldValidate: true }); + }; + + // Render selected author option + const renderAuthorOption = ( + option: SelectOption, + { focus, selected }: { selected: boolean; focus: boolean } + ) => { + const authorData = option.data; + + return ( +
  • +
    +
    + {authorData?.profileImage ? ( +
    + {authorData.fullName +
    + ) : ( +
    + +
    + )} +
    +
    +

    {option.label}

    + {authorData?.headline && ( +

    {authorData.headline}

    + )} +
    +
    +
  • + ); + }; return (
    Authors - setValue('authors', newAuthors, { shouldValidate: true })} - options={authorOptions} - placeholder={isLoadingUsers ? 'Loading authors...' : 'Search for authors...'} - disabled={isLoadingUsers} - error={getFieldErrorMessage(errors.authors, 'Invalid authors')} - /> + + {/* Author Search */} +
    + + value={null} + onChange={handleAuthorSelect} + onSearch={handleSearchAuthors} + placeholder={isLoadingUsers ? 'Loading authors...' : 'Search for authors...'} + disabled={isLoadingUsers} + error={getFieldErrorMessage(errors.authors, 'Invalid authors')} + debounceMs={300} + minSearchLength={2} + renderOption={renderAuthorOption} + /> +
    + + {/* Selected Authors Display */} + {authors.length > 0 && ( +
    +

    Selected Authors:

    +
    + {authors.map((author: any) => ( +
    +
    +
    + {author.data?.profileImage ? ( +
    + {author.data.fullName +
    + ) : ( +
    + +
    + )} +
    +
    +

    + {author.label} + {author.data?.userId === currentUser?.id?.toString() && ( + (you) + )} +

    + {author.data?.headline && ( +

    {author.data.headline}

    + )} +
    +
    + +
    + ))} +
    +
    + )}
    ); } diff --git a/app/notebook/components/PublishingForm/components/ContactsSection.tsx b/app/notebook/components/PublishingForm/components/ContactsSection.tsx index 69de59cde..719e8aef5 100644 --- a/app/notebook/components/PublishingForm/components/ContactsSection.tsx +++ b/app/notebook/components/PublishingForm/components/ContactsSection.tsx @@ -1,15 +1,13 @@ -import { Users } from 'lucide-react'; +import { Users, X } from 'lucide-react'; import { SectionHeader } from './SectionHeader'; -import { - SearchableMultiSelect, - MultiSelectOption, -} from '@/components/ui/form/SearchableMultiSelect'; -import { useCallback } from 'react'; +import { AutocompleteSelect, SelectOption } from '@/components/ui/form/AutocompleteSelect'; import { useFormContext } from 'react-hook-form'; import { useNotebookContext } from '@/contexts/NotebookContext'; import { getFieldErrorMessage } from '@/utils/form'; +import { useUser } from '@/contexts/UserContext'; import { SearchService } from '@/services/search.service'; import { AuthorSuggestion } from '@/types/search'; +import { Button } from '@/components/ui/Button'; export function ContactsSection() { const { @@ -18,38 +16,169 @@ export function ContactsSection() { formState: { errors }, } = useFormContext(); + const { isLoadingUsers } = useNotebookContext(); + const { user: currentUser } = useUser(); + const contacts = watch('contacts') || []; - // Helper function for async people search - const handleAsyncSearch = useCallback(async (query: string): Promise => { - try { - const authorSuggestions: AuthorSuggestion[] = await SearchService.suggestPeople(query); + // Search function for AutocompleteSelect + const handleSearchContacts = async (query: string): Promise[]> => { + if (!query.trim()) return []; - // Filter out suggestions without userId and map to options - return authorSuggestions - .filter((author) => author.userId) + try { + const results: AuthorSuggestion[] = await SearchService.suggestPeople(query); + return results + .filter((author) => author.userId) // Filter out suggestions without userId .map((author) => ({ value: author.userId?.toString() || `temp-${Date.now()}`, label: author.fullName || 'Unknown User', + data: author, })); } catch (error) { - console.error('Error searching people:', error); + console.error('Error searching contacts:', error); return []; } - }, []); + }; + + // Handle contact selection + const handleContactSelect = (selectedOption: SelectOption | null) => { + if (selectedOption) { + // Check if contact is already selected + const isAlreadySelected = contacts.some( + (contact: any) => contact.value === selectedOption.value + ); + + if (!isAlreadySelected) { + const newContacts = [...contacts, selectedOption]; + setValue('contacts', newContacts, { shouldValidate: true }); + } + } + }; + + // Handle contact removal + const handleRemoveContact = (contactToRemove: SelectOption) => { + const newContacts = contacts.filter((contact: any) => contact.value !== contactToRemove.value); + setValue('contacts', newContacts, { shouldValidate: true }); + }; + + // Render selected contact option + const renderContactOption = ( + option: SelectOption, + { focus, selected }: { selected: boolean; focus: boolean } + ) => { + const contactData = option.data; + + return ( +
  • +
    +
    + {contactData?.profileImage ? ( +
    + {contactData.fullName +
    + ) : ( +
    + +
    + )} +
    +
    +

    {option.label}

    + {contactData?.headline && ( +

    {contactData.headline}

    + )} +
    +
    +
  • + ); + }; return (
    Contacts - setValue('contacts', newContacts, { shouldValidate: true })} - onAsyncSearch={handleAsyncSearch} - placeholder="Search for contacts..." - error={getFieldErrorMessage(errors.contacts, 'Invalid contacts')} - debounceMs={500} - helperText="Add contacts who will be responsible for managing this grant. These contacts will receive important updates about the grant's progress." - /> + + {/* Contact Search */} +
    + + value={null} + onChange={handleContactSelect} + onSearch={handleSearchContacts} + placeholder={isLoadingUsers ? 'Loading contacts...' : 'Search for contacts...'} + disabled={isLoadingUsers} + error={getFieldErrorMessage(errors.contacts, 'Invalid contacts')} + debounceMs={300} + minSearchLength={2} + renderOption={renderContactOption} + /> +
    + + {/* Selected Contacts Display */} + {contacts.length > 0 && ( +
    +

    Selected Contacts:

    +
    + {contacts.map((contact: any) => ( +
    +
    +
    + {contact.data?.profileImage ? ( +
    + {contact.data.fullName +
    + ) : ( +
    + +
    + )} +
    +
    +

    + {contact.label} + {contact.data?.userId === currentUser?.id?.toString() && ( + (you) + )} +

    + {contact.data?.headline && ( +

    {contact.data.headline}

    + )} +
    +
    + +
    + ))} +
    +
    + )} + + {/* Helper Text */} +

    + Add contacts who will be responsible for managing this grant. These contacts will receive + important updates about the grant's progress. +

    ); } diff --git a/app/notebook/components/PublishingForm/index.tsx b/app/notebook/components/PublishingForm/index.tsx index dbc7ad8af..fc16aa79f 100644 --- a/app/notebook/components/PublishingForm/index.tsx +++ b/app/notebook/components/PublishingForm/index.tsx @@ -33,6 +33,7 @@ import { useNotebookContext } from '@/contexts/NotebookContext'; import { useAssetUpload } from '@/hooks/useAssetUpload'; import { useNonprofitLink } from '@/hooks/useNonprofitLink'; import { NonprofitConfirmModal } from '@/components/Nonprofit'; +import { useUser } from '@/contexts/UserContext'; // Feature flags for conditionally showing sections const FEATURE_FLAG_RESEARCH_COIN = false; @@ -75,7 +76,8 @@ const getButtonText = ({ }; export function PublishingForm({ bountyAmount, onBountyClick }: PublishingFormProps) { - const { currentNote: note, editor } = useNotebookContext(); + const { currentNote: note, editor, users } = useNotebookContext(); + const { user: currentUser } = useUser(); const searchParams = useSearchParams(); const [isRedirecting, setIsRedirecting] = useState(false); const [{ loading: isUploadingImage }, uploadAsset] = useAssetUpload(); @@ -126,6 +128,7 @@ export function PublishingForm({ bountyAmount, onBountyClick }: PublishingFormPr // 1. note.post data // 2. localStorage data // 3. URL search params + // 4. Auto-add current user as author/contact if none exist useEffect(() => { if (!note) return; @@ -235,6 +238,45 @@ export function PublishingForm({ bountyAmount, onBountyClick }: PublishingFormPr methods.setValue('articleType', articleType); } } + + // Priority 4: Auto-add current user as author/contact if none exist + // Only do this if we're using localStorage data or if there's no data at all + if ((storedData || (!note?.post && !storedData)) && users?.users && currentUser) { + const currentAuthor = users.users.find((user) => user.id === currentUser.id.toString()); + + if (currentAuthor) { + const userOption = { + value: currentAuthor.authorId.toString(), + label: currentAuthor.name || currentAuthor.email || 'Unknown User', + data: { + id: currentAuthor.authorId, + fullName: currentAuthor.name || currentAuthor.email || 'Unknown User', + profileImage: currentAuthor.avatarUrl, + userId: currentAuthor.id, + institutions: [], + education: [], + reputationHubs: [], + }, + }; + + // Get current article type to determine whether to add as author or contact + const articleType = methods.getValues('articleType'); + + if (articleType === 'grant') { + // For grants: add as contact if no contacts exist + const currentContacts = methods.getValues('contacts'); + if (currentContacts.length === 0) { + methods.setValue('contacts', [userOption], { shouldValidate: true }); + } + } else { + // For non-grants: add as author if no authors exist + const currentAuthors = methods.getValues('authors'); + if (currentAuthors.length === 0) { + methods.setValue('authors', [userOption], { shouldValidate: true }); + } + } + } + } }, [note, methods, searchParams]); // Add effect to save form data when it changes From 89fc271f19c888649c74319b9771c437d48984dd Mon Sep 17 00:00:00 2001 From: Nick Tytarenko Date: Thu, 7 Aug 2025 12:08:45 +0300 Subject: [PATCH 2/5] WIP - use static list for authors --- .../components/AuthorsSection.tsx | 155 ++++++++++++++---- .../components/PublishingForm/index.tsx | 16 +- .../components/PublishingForm/schema.ts | 1 + components/AvatarUpload.tsx | 2 +- components/modals/ImageUploadModal.tsx | 44 ++--- .../modals/OrganizationSettingsModal.tsx | 2 +- types/note.ts | 2 + types/organization.ts | 2 +- types/search.ts | 2 +- types/topic.ts | 2 +- 10 files changed, 153 insertions(+), 75 deletions(-) diff --git a/app/notebook/components/PublishingForm/components/AuthorsSection.tsx b/app/notebook/components/PublishingForm/components/AuthorsSection.tsx index 6b7ed57a4..11c1d67b8 100644 --- a/app/notebook/components/PublishingForm/components/AuthorsSection.tsx +++ b/app/notebook/components/PublishingForm/components/AuthorsSection.tsx @@ -5,9 +5,22 @@ import { useFormContext } from 'react-hook-form'; import { useNotebookContext } from '@/contexts/NotebookContext'; import { getFieldErrorMessage } from '@/utils/form'; import { useUser } from '@/contexts/UserContext'; -import { SearchService } from '@/services/search.service'; -import { AuthorSuggestion } from '@/types/search'; import { Button } from '@/components/ui/Button'; +import { OrganizationMember } from '@/types/organization'; +import { useInviteUserToOrg } from '@/hooks/useOrganization'; +import { useOrganizationContext } from '@/contexts/OrganizationContext'; +import { useSession } from 'next-auth/react'; +import { isValidEmail } from '@/utils/validation'; +import { toast } from 'react-hot-toast'; + +// Create a type for organization member as author suggestion +interface OrgMemberAuthorSuggestion { + id: string; + fullName: string; + profileImage?: string; + email: string; + role: string; +} export function AuthorsSection() { const { @@ -16,30 +29,60 @@ export function AuthorsSection() { formState: { errors }, } = useFormContext(); - const { isLoadingUsers } = useNotebookContext(); + const { isLoadingUsers, users: orgUsers, refreshUsers } = useNotebookContext(); const { user: currentUser } = useUser(); + const { selectedOrg: organization } = useOrganizationContext(); + const { data: session } = useSession(); + const [{ isLoading: isInvitingUser }, inviteUserToOrg] = useInviteUserToOrg(); const authors = watch('authors') || []; - // Search function for AutocompleteSelect - const handleSearchAuthors = async (query: string): Promise[]> => { - if (!query.trim()) return []; + // Check if current user is admin + const isCurrentUserAdmin = (() => { + if (!session?.userId || !orgUsers?.users) return false; + const currentUser = orgUsers.users.find((user) => user.id === session.userId.toString()); + return currentUser?.role === 'ADMIN'; + })(); - try { - const results: AuthorSuggestion[] = await SearchService.suggestPeople(query); - return results.map((author) => ({ - value: author.id?.toString() || author.fullName || Date.now().toString(), - label: author.fullName || 'Unnamed Author', + // Transform organization users to author suggestions + const orgMemberAuthors: OrgMemberAuthorSuggestion[] = + orgUsers?.users?.map((user: OrganizationMember) => ({ + id: user.id, + fullName: user.name, + profileImage: user.avatarUrl, + email: user.email, + role: user.role, + })) || []; + + // Simple filtering function for organization users + const handleSearchAuthors = async ( + query: string + ): Promise[]> => { + if (!query.trim()) { + // Return all org users when no query + return orgMemberAuthors.map((author) => ({ + value: author.id, + label: author.fullName, data: author, })); - } catch (error) { - console.error('Error searching authors:', error); - return []; } + + // Simple case-insensitive filtering by name or email + const filteredAuthors = orgMemberAuthors.filter( + (author) => + author.fullName.toLowerCase().includes(query.toLowerCase()) || + author.email.toLowerCase().includes(query.toLowerCase()) + ); + + return filteredAuthors.map((author) => ({ + value: author.id, + label: author.fullName, + data: author, + })); }; // Handle author selection - const handleAuthorSelect = (selectedOption: SelectOption | null) => { + const handleAuthorSelect = (selectedOption: SelectOption | null) => { if (selectedOption) { // Check if author is already selected const isAlreadySelected = authors.some( @@ -47,21 +90,68 @@ export function AuthorsSection() { ); if (!isAlreadySelected) { - const newAuthors = [...authors, selectedOption]; + const newAuthors = [ + ...authors, + { ...selectedOption, image: selectedOption.data?.profileImage }, + ]; setValue('authors', newAuthors, { shouldValidate: true }); } } }; // Handle author removal - const handleRemoveAuthor = (authorToRemove: SelectOption) => { + const handleRemoveAuthor = (authorToRemove: SelectOption) => { const newAuthors = authors.filter((author: any) => author.value !== authorToRemove.value); setValue('authors', newAuthors, { shouldValidate: true }); }; + // Handle inviting a new user + const handleInviteAuthor = async ( + query: string + ): Promise | null> => { + if (!isValidEmail(query)) { + toast.error('Please enter a valid email address'); + return null; + } + + if (!organization) { + toast.error('Organization not found'); + return null; + } + + if (!isCurrentUserAdmin) { + toast.error('Only admins can invite users'); + return null; + } + + // Check if user is already a member + const isAlreadyMember = orgMemberAuthors.some( + (author) => author.email.toLowerCase() === query.toLowerCase() + ); + if (isAlreadyMember) { + toast.error('This user is already a member of the organization'); + return null; + } + + try { + await inviteUserToOrg(organization.id, query); + toast.success(`Invitation sent to ${query}`); + + // Refresh the organization users list to show the new invite + await refreshUsers(true); + + // Return null since the invited user won't be immediately available for selection + // They'll need to accept the invitation first + return null; + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to invite user'); + return null; + } + }; + // Render selected author option const renderAuthorOption = ( - option: SelectOption, + option: SelectOption, { focus, selected }: { selected: boolean; focus: boolean } ) => { const authorData = option.data; @@ -72,7 +162,7 @@ export function AuthorsSection() { focus ? 'bg-gray-100' : 'text-gray-900' }`} > -
    +
    {authorData?.profileImage ? (
    @@ -90,9 +180,7 @@ export function AuthorsSection() {

    {option.label}

    - {authorData?.headline && ( -

    {authorData.headline}

    - )} +

    {authorData?.email}

    @@ -105,7 +193,7 @@ export function AuthorsSection() { {/* Author Search */}
    - + value={null} onChange={handleAuthorSelect} onSearch={handleSearchAuthors} @@ -113,8 +201,11 @@ export function AuthorsSection() { disabled={isLoadingUsers} error={getFieldErrorMessage(errors.authors, 'Invalid authors')} debounceMs={300} - minSearchLength={2} + minSearchLength={1} renderOption={renderAuthorOption} + allowCreatingNew={isCurrentUserAdmin} + onCreateNew={handleInviteAuthor} + createNewLabel="Invite: " />
    @@ -128,13 +219,13 @@ export function AuthorsSection() { key={author.value} className="flex items-center justify-between px-2 py-1 bg-gray-50 rounded-lg border border-gray-200" > -
    +
    - {author.data?.profileImage ? ( + {author.image ? (
    {author.data.fullName
    @@ -145,14 +236,14 @@ export function AuthorsSection() { )}
    -

    +

    {author.label} - {author.data?.userId === currentUser?.id?.toString() && ( + {author.data?.id === currentUser?.id?.toString() && ( (you) )}

    - {author.data?.headline && ( -

    {author.data.headline}

    + {author.data?.email && ( +

    {author.data.email}

    )}
    diff --git a/app/notebook/components/PublishingForm/index.tsx b/app/notebook/components/PublishingForm/index.tsx index fc16aa79f..305ba87a2 100644 --- a/app/notebook/components/PublishingForm/index.tsx +++ b/app/notebook/components/PublishingForm/index.tsx @@ -195,10 +195,16 @@ export function PublishingForm({ bountyAmount, onBountyClick }: PublishingFormPr } if (note.post.authors && note.post.authors.length > 0) { - const authorOptions = note.post.authors.map((author) => ({ - value: author.authorId.toString(), - label: author.name, - })); + const authorOptions = note.post.authors.map((author) => { + const orgUser = users?.users.find((user) => user.authorId === author.authorId); + console.log('orgUser', orgUser); + + return { + value: author.authorId.toString(), + label: author.name, + image: orgUser?.avatarUrl, + }; + }); methods.setValue('authors', authorOptions); } @@ -277,7 +283,7 @@ export function PublishingForm({ bountyAmount, onBountyClick }: PublishingFormPr } } } - }, [note, methods, searchParams]); + }, [note, methods, searchParams, users]); // Add effect to save form data when it changes useEffect(() => { diff --git a/app/notebook/components/PublishingForm/schema.ts b/app/notebook/components/PublishingForm/schema.ts index f11986816..07202019e 100644 --- a/app/notebook/components/PublishingForm/schema.ts +++ b/app/notebook/components/PublishingForm/schema.ts @@ -4,6 +4,7 @@ import { NonprofitOrg } from '@/types/nonprofit'; const optionSchema = z.object({ value: z.string(), label: z.string(), + image: z.string().optional(), }); export const publishingFormSchema = z diff --git a/components/AvatarUpload.tsx b/components/AvatarUpload.tsx index a79765327..af547e131 100644 --- a/components/AvatarUpload.tsx +++ b/components/AvatarUpload.tsx @@ -135,7 +135,7 @@ export const AvatarUpload = ({
    ) : ( -
    +

    Drag and drop an image here, or click to select

    -
    - -
    - - - {error && ( -
    {error}
    - )} -
    - -
    - + {error &&
    {error}
    } + ); } diff --git a/components/modals/OrganizationSettingsModal.tsx b/components/modals/OrganizationSettingsModal.tsx index 37812aef6..723e1a3ee 100644 --- a/components/modals/OrganizationSettingsModal.tsx +++ b/components/modals/OrganizationSettingsModal.tsx @@ -202,7 +202,7 @@ export function OrganizationSettingsModal({ isOpen, onClose }: OrganizationSetti const isCurrentUserAdmin = (() => { if (!session?.userId || !orgUsers?.users) return false; - const currentUser = orgUsers.users.find((user) => user.id.toString() === session.userId); + const currentUser = orgUsers.users.find((user) => user.id === session.userId.toString()); return currentUser?.role === 'ADMIN'; })(); diff --git a/types/note.ts b/types/note.ts index 908188158..805cda38e 100644 --- a/types/note.ts +++ b/types/note.ts @@ -13,6 +13,7 @@ export type Author = { authorId: number; userId: number; name: string; + profileImage?: string; }; export type Contact = { @@ -73,6 +74,7 @@ export const transformAuthor = createTransformer((raw: any) => ({ authorId: raw.id, userId: raw.user, name: `${raw.first_name || ''} ${raw.last_name || ''}`.trim() || 'Unknown', + profileImage: raw.profile_image, })); export const transformContact = createTransformer((raw) => ({ diff --git a/types/organization.ts b/types/organization.ts index 96715c93b..196b88e5c 100644 --- a/types/organization.ts +++ b/types/organization.ts @@ -43,7 +43,7 @@ function transformOrganizationUser(raw: any, role: OrganizationRole): Organizati name: `${raw.author_profile?.first_name} ${raw.author_profile?.last_name}`.trim(), email: raw.email, role, - avatarUrl: raw.author_profile?.profileImage || undefined, + avatarUrl: raw.author_profile?.profile_image || undefined, }; } diff --git a/types/search.ts b/types/search.ts index c8f2db6d4..16a4cd5ac 100644 --- a/types/search.ts +++ b/types/search.ts @@ -283,7 +283,7 @@ export const transformAuthorSuggestion = (raw: any): AuthorSuggestion => { export const transformAuthorSuggestions = (raw: any): AuthorSuggestion[] => { const authorSuggestions: AuthorSuggestion[] = []; - const suggestions = raw.suggestion_phrases__completion; + const suggestions = raw.suggestion_phrases__completion || raw.suggestion_phrases || []; if (Array.isArray(suggestions)) { suggestions.forEach((suggestion: any) => { diff --git a/types/topic.ts b/types/topic.ts index 3c4f2d6a6..98f3e383c 100644 --- a/types/topic.ts +++ b/types/topic.ts @@ -39,7 +39,7 @@ export const transformTopic = createTransformer((raw: any) => { export const transformTopicSuggestions = (raw: any): Topic[] => { const topicSuggestions: Topic[] = []; - const suggestions = raw.name_suggest__completion; + const suggestions = raw.name_suggest__completion || raw.name_suggest || []; suggestions.forEach((suggestion: any) => { suggestion.options.forEach((option: any) => { From 15b6c7c04b420049fb1c78859ce7888385935a7e Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Thu, 7 Aug 2025 22:19:16 +0300 Subject: [PATCH 3/5] update the Avatar Uploader component to use Base modal --- app/api/auth/[...nextauth]/auth.config.ts | 2 +- components/AvatarUpload.tsx | 187 ++++++++---------- components/modals/ImageUploadModal.tsx | 2 - .../modals/OrganizationSettingsModal.tsx | 4 +- 4 files changed, 90 insertions(+), 105 deletions(-) diff --git a/app/api/auth/[...nextauth]/auth.config.ts b/app/api/auth/[...nextauth]/auth.config.ts index 5266daef5..37d90d869 100644 --- a/app/api/auth/[...nextauth]/auth.config.ts +++ b/app/api/auth/[...nextauth]/auth.config.ts @@ -133,7 +133,7 @@ export const authOptions: NextAuthOptions = { ...session, authToken: token.authToken, isLoggedIn: true, - userId: token.sub, + userId: token.sub?.toString(), }; } catch (error) { console.error('Session callback failed:', error); diff --git a/components/AvatarUpload.tsx b/components/AvatarUpload.tsx index af547e131..a411acdd7 100644 --- a/components/AvatarUpload.tsx +++ b/components/AvatarUpload.tsx @@ -8,7 +8,6 @@ import AvatarEditor from 'react-avatar-editor'; import { Slider } from '@/components/ui/Slider'; interface AvatarUploadProps { - isOpen: boolean; onClose: () => void; onSave: (imageDataUrl: string) => Promise; initialImage?: string | null; @@ -17,7 +16,6 @@ interface AvatarUploadProps { } export const AvatarUpload = ({ - isOpen, onClose, onSave, initialImage, @@ -65,109 +63,98 @@ export const AvatarUpload = ({ setRotate((prevRotate) => prevRotate + 90); }; - if (!isOpen) return null; - return ( -
    -
    -
    -

    Upload Picture

    - -
    - -
    - {image ? ( -
    -
    - +
    +
    + {image ? ( +
    +
    + +
    +
    +
    + + Zoom +
    -
    -
    - - Zoom - -
    - setScale(values[0])} - className="w-full" - /> -
    - - -
    + setScale(values[0])} + className="w-full" + /> +
    + +
    - ) : ( -
    - -

    Drag and drop an image here, or click to select

    - -
    - )} - -
    +
    + ) : ( +
    + +

    Drag and drop an image here, or click to select

    + +
    + )} + +
    -
    - - -
    +
    + +
    ); diff --git a/components/modals/ImageUploadModal.tsx b/components/modals/ImageUploadModal.tsx index a9daa752d..15f3ed224 100644 --- a/components/modals/ImageUploadModal.tsx +++ b/components/modals/ImageUploadModal.tsx @@ -52,13 +52,11 @@ export function ImageUploadModal({ return ( - {error &&
    {error}
    }
    ); diff --git a/components/modals/OrganizationSettingsModal.tsx b/components/modals/OrganizationSettingsModal.tsx index 723e1a3ee..e36deae6d 100644 --- a/components/modals/OrganizationSettingsModal.tsx +++ b/components/modals/OrganizationSettingsModal.tsx @@ -60,7 +60,7 @@ const UserRow = ({ }) => { const isCurrentUser = () => { if (!sessionUserId || user.type === 'invite') return false; - return sessionUserId === user.id.toString(); + return sessionUserId.toString() === user.id.toString(); }; return ( @@ -405,7 +405,7 @@ export function OrganizationSettingsModal({ isOpen, onClose }: OrganizationSetti {/* Organization Name */}
    -
    +