Repository: liam-hq/liam
Pull request #1376: ✨ Display last repository commit time and organization icon in ProjectItem
Author: login: devin-ai-integration[bot]
Labels: name: Review effort 3/5
Created: 2025-04-16T12:53:04Z | Merged: 2025-04-18T08:04:01Z




https://github.com/user-attachments/assets/bc09f1ce-0fa6-413c-9941-038203a39a10


# Display last repository commit time and organization icon in ProjectItem

## Overview
- Display the last commit time of the repository linked to a project instead of project creation time
- Show the GitHub organization icon for the repository in the project icon placeholder

## Changes
- Added  and  functions to GitHub API
- Modified project data retrieval to include repository information
- Created server components to fetch and display repository data
- Updated ProjectItem component to show repository information

Link to Devin run: https://app.devin.ai/sessions/8077ac441097437a90cb905785b91dbd
User: noritaka.ikeda@route06.co.jp



Diff:
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/LastCommitData.tsx b/frontend/apps/app/features/projects/components/ProjectItem/LastCommitData.tsx
new file mode 100644
index 0000000000..cf90e38f39
--- /dev/null
+++ b/frontend/apps/app/features/projects/components/ProjectItem/LastCommitData.tsx
@@ -0,0 +1,30 @@
+'use server'
+
+import { getLastCommit } from '@liam-hq/github'
+
+// define the data fetching function as a server action
+export async function fetchLastCommitData(
+  installationId: number,
+  owner: string,
+  repo: string,
+) {
+  try {
+    if (!installationId || !owner || !repo) {
+      return null
+    }
+
+    const commitInfo = await getLastCommit(installationId, owner, repo)
+
+    if (commitInfo) {
+      return {
+        author: commitInfo.author,
+        date: commitInfo.date,
+      }
+    }
+
+    return null
+  } catch (error) {
+    console.error('Failed to fetch last commit info:', error)
+    return null
+  }
+}
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/LastCommitDataWrapper.tsx b/frontend/apps/app/features/projects/components/ProjectItem/LastCommitDataWrapper.tsx
new file mode 100644
index 0000000000..f617178339
--- /dev/null
+++ b/frontend/apps/app/features/projects/components/ProjectItem/LastCommitDataWrapper.tsx
@@ -0,0 +1,75 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { fetchLastCommitData } from './LastCommitData'
+
+interface LastCommitDataWrapperProps {
+  installationId: number
+  owner: string
+  repo: string
+  defaultDate: string
+}
+
+interface CommitInfo {
+  author: string
+  date: string
+}
+
+export function LastCommitDataWrapper({
+  installationId,
+  owner,
+  repo,
+  defaultDate,
+}: LastCommitDataWrapperProps) {
+  const [commitInfo, setCommitInfo] = useState<CommitInfo | null>(null)
+  const [isLoading, setIsLoading] = useState(true)
+
+  // Date formatting function
+  const formatDate = (dateString: string) => {
+    const date = new Date(dateString)
+    return date.toLocaleDateString('en-US', {
+      month: 'short',
+      day: 'numeric',
+      year: 'numeric',
+    })
+  }
+
+  useEffect(() => {
+    async function loadData() {
+      try {
+        const data = await fetchLastCommitData(installationId, owner, repo)
+        setCommitInfo(data)
+      } catch (error) {
+        console.error('Error fetching commit data:', error)
+      } finally {
+        setIsLoading(false)
+      }
+    }
+
+    loadData()
+  }, [installationId, owner, repo])
+
+  if (isLoading) {
+    return <span>Loading commit info...</span>
+  }
+
+  // When commit information is available
+  if (commitInfo) {
+    return (
+      <>
+        <span>{commitInfo.author}</span>
+        <span>committed</span>
+        <span>on {formatDate(commitInfo.date)}</span>
+      </>
+    )
+  }
+
+  // Default display (when fetch fails)
+  return (
+    <>
+      <span>User</span>
+      <span>committed</span>
+      <span>on {formatDate(defaultDate)}</span>
+    </>
+  )
+}
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/OrganizationData.tsx b/frontend/apps/app/features/projects/components/ProjectItem/OrganizationData.tsx
new file mode 100644
index 0000000000..97d382bda5
--- /dev/null
+++ b/frontend/apps/app/features/projects/components/ProjectItem/OrganizationData.tsx
@@ -0,0 +1,45 @@
+'use server'
+
+import { getOrganizationInfo } from '@liam-hq/github'
+import { OrganizationIcon } from './OrganizationIcon'
+import { ProjectIcon } from './ProjectIcon'
+import styles from './ProjectItem.module.css'
+
+interface OrganizationDataProps {
+  installationId: number
+  owner: string
+  repo: string
+}
+
+// Separating data fetching function as a server action
+export async function fetchOrganizationData(
+  installationId: number,
+  owner: string,
+  repo: string,
+) {
+  try {
+    if (!installationId || !owner || !repo) {
+      return null
+    }
+    const orgInfo = await getOrganizationInfo(installationId, owner, repo)
+    return orgInfo?.avatar_url || null
+  } catch (error) {
+    console.error('Failed to fetch organization info:', error)
+    return null
+  }
+}
+
+// Server component
+export async function OrganizationData({
+  installationId,
+  owner,
+  repo,
+}: OrganizationDataProps) {
+  const avatarUrl = await fetchOrganizationData(installationId, owner, repo)
+
+  if (avatarUrl) {
+    return <OrganizationIcon avatarUrl={avatarUrl} owner={owner} />
+  }
+
+  return <ProjectIcon className={styles.projectIcon} />
+}
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/OrganizationDataWrapper.tsx b/frontend/apps/app/features/projects/components/ProjectItem/OrganizationDataWrapper.tsx
new file mode 100644
index 0000000000..8a83a44d11
--- /dev/null
+++ b/frontend/apps/app/features/projects/components/ProjectItem/OrganizationDataWrapper.tsx
@@ -0,0 +1,47 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { fetchOrganizationData } from './OrganizationData'
+import { OrganizationIcon } from './OrganizationIcon'
+import { ProjectIcon } from './ProjectIcon'
+import styles from './ProjectItem.module.css'
+
+interface OrganizationDataWrapperProps {
+  installationId: number
+  owner: string
+  repo: string
+}
+
+export function OrganizationDataWrapper({
+  installationId,
+  owner,
+  repo,
+}: OrganizationDataWrapperProps) {
+  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
+  const [isLoading, setIsLoading] = useState(true)
+
+  useEffect(() => {
+    async function loadData() {
+      try {
+        const data = await fetchOrganizationData(installationId, owner, repo)
+        setAvatarUrl(data)
+      } catch (error) {
+        console.error('Error fetching organization data:', error)
+      } finally {
+        setIsLoading(false)
+      }
+    }
+
+    loadData()
+  }, [installationId, owner, repo])
+
+  if (isLoading) {
+    return <ProjectIcon className={styles.projectIcon} />
+  }
+
+  return avatarUrl ? (
+    <OrganizationIcon avatarUrl={avatarUrl} owner={owner} />
+  ) : (
+    <ProjectIcon className={styles.projectIcon} />
+  )
+}
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/OrganizationIcon.tsx b/frontend/apps/app/features/projects/components/ProjectItem/OrganizationIcon.tsx
new file mode 100644
index 0000000000..f9ff3d5d66
--- /dev/null
+++ b/frontend/apps/app/features/projects/components/ProjectItem/OrganizationIcon.tsx
@@ -0,0 +1,26 @@
+'use client'
+
+import Image from 'next/image'
+import { ProjectIcon } from './ProjectIcon'
+import styles from './ProjectItem.module.css'
+
+interface OrganizationIconProps {
+  avatarUrl?: string | null
+  owner: string
+}
+
+export function OrganizationIcon({ avatarUrl, owner }: OrganizationIconProps) {
+  if (avatarUrl) {
+    return (
+      <Image
+        src={avatarUrl}
+        alt={`${owner} organization icon`}
+        width={32}
+        height={32}
+        className={styles.projectIcon}
+      />
+    )
+  }
+
+  return <ProjectIcon className={styles.projectIcon} />
+}
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.module.css b/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.module.css
index d3ee53992c..6d04095468 100644
--- a/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.module.css
+++ b/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.module.css
@@ -36,6 +36,19 @@
   overflow: hidden;
 }
 
+.projectIconPlaceholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.projectIconPlaceholder svg {
+  width: 100%;
+  height: 100%;
+}
+
 .projectName {
   font-size: var(--font-size-5);
   font-weight: 600;
diff --git a/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx b/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
index 01f3f1b9b5..2af11e2cd0 100644
--- a/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
+++ b/frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
@@ -1,18 +1,21 @@
 import { urlgen } from '@/utils/routes'
+import type { Tables } from '@liam-hq/db/supabase/database.types'
 import { GithubLogo } from '@liam-hq/ui'
 import Link from 'next/link'
 import type { FC } from 'react'
+import { LastCommitDataWrapper } from './LastCommitDataWrapper'
+import { OrganizationDataWrapper } from './OrganizationDataWrapper'
 import { ProjectIcon } from './ProjectIcon'
 import styles from './ProjectItem.module.css'
 
-interface Project {
-  id: string
-  name: string
-  created_at: string
+type ProjectWithRepositories = Tables<'projects'> & {
+  project_repository_mappings?: Array<{
+    repository: Tables<'repositories'>
+  }>
 }
 
 interface ProjectItemProps {
-  project: Project
+  project: ProjectWithRepositories
 }
 
 export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
@@ -26,6 +29,9 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.project_repository_mappings?.[0]?.repository
+
   return (
     <Link
       href={urlgen('projects/[projectId]', {
@@ -34,7 +40,19 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
       className={styles.projectItem}
     >
       <div className={styles.projectHeader}>
-        <ProjectIcon className={styles.projectIcon} />
+        <div className={styles.projectIcon}>
+          <div className={styles.projectIconPlaceholder}>
+            {repository ? (
+              <OrganizationDataWrapper
+                installationId={repository.installation_id}
+                owner={repository.owner}
+                repo={repository.name}
+              />
+            ) : (
+              <ProjectIcon className={styles.projectIcon} />
+            )}
+          </div>
+        </div>
         <h2 className={styles.projectName}>{project.name}</h2>
       </div>
 
@@ -42,16 +60,27 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
         <div className={styles.repositoryBadge}>
           <GithubLogo className={styles.repositoryIcon} />
           <span className={styles.repositoryName}>
-            {project.name
-              ? `${project.name.toLowerCase()}`
-              : 'untitled-project'}
+            {repository
+              ? `${repository.owner}/${repository.name}`
+              : repositoryName}
           </span>
         </div>
 
         <div className={styles.commitInfo}>
-          <span>User</span>
-          <span>committed</span>
-          <span>on {formatDate(project.created_at)}</span>
+          {repository ? (
+            <LastCommitDataWrapper
+              installationId={repository.installation_id}
+              owner={repository.owner}
+              repo={repository.name}
+              defaultDate={project.created_at}
+            />
+          ) : (
+            <>
+              <span>User</span>
+              <span>committed</span>
+              <span>on {formatDate(project.created_at)}</span>
+            </>
+          )}
         </div>
       </div>
     </Link>
diff --git a/frontend/apps/app/features/projects/pages/ProjectsPage/getProjects.ts b/frontend/apps/app/features/projects/pages/ProjectsPage/getProjects.ts
index 6e0fa69f6e..8a6e49123c 100644
--- a/frontend/apps/app/features/projects/pages/ProjectsPage/getProjects.ts
+++ b/frontend/apps/app/features/projects/pages/ProjectsPage/getProjects.ts
@@ -5,7 +5,21 @@ export const getProjects = async (organizationId?: string) => {
 
   let query = supabase
     .from('projects')
-    .select('id, name, created_at, updated_at, organization_id')
+    .select(`
+      id,
+      name,
+      created_at,
+      updated_at,
+      organization_id,
+      project_repository_mappings:project_repository_mappings(
+        repository:repositories(
+          id,
+          name,
+          owner,
+          installation_id
+        )
+      )
+    `)
     .order('id', { ascending: false })
 
   if (organizationId) {
diff --git a/frontend/apps/app/next.config.ts b/frontend/apps/app/next.config.ts
index c907c1dc1e..333bf5dcee 100644
--- a/frontend/apps/app/next.config.ts
+++ b/frontend/apps/app/next.config.ts
@@ -16,6 +16,9 @@ const nextConfig: NextConfig = {
   // NOTE: Exclude Prisma-related packages from the bundle
   // These packages are installed separately in the node_modules/@prisma directory
   // Excluding them prevents `Error: Cannot find module 'fs'` errors in the build process
+  images: {
+    domains: ['avatars.githubusercontent.com'],
+  },
   webpack: (config) => {
     if (Array.isArray(config.externals)) {
       config.externals.push(
diff --git a/frontend/packages/github/src/api.server.ts b/frontend/packages/github/src/api.server.ts
index 4104627b24..717f9d64cd 100644
--- a/frontend/packages/github/src/api.server.ts
+++ b/frontend/packages/github/src/api.server.ts
@@ -275,3 +275,84 @@ export const createOrUpdateFileContent = async (
     return { success: false, sha: null }
   }
 }
+
+/**
+ * Gets the latest commit information for a repository
+ * @returns Latest commit details or null
+ */
+export const getLastCommit = async (
+  installationId: number,
+  owner: string,
+  repo: string,
+  branch = 'main',
+): Promise<{
+  sha: string
+  date: string
+  message: string
+  author: string
+} | null> => {
+  const octokit = await createOctokit(installationId)
+
+  try {
+    const { data: commits } = await octokit.repos.listCommits({
+      owner,
+      repo,
+      sha: branch,
+      per_page: 1, // Only need the latest commit
+    })
+
+    if (!commits || commits.length === 0) {
+      return null
+    }
+
+    const latestCommit = commits[0]
+    if (!latestCommit || !latestCommit.commit) {
+      return null
+    }
+
+    return {
+      sha: latestCommit.sha || '',
+      date:
+        latestCommit.commit.committer?.date ||
+        latestCommit.commit.author?.date ||
+        '',
+      message: latestCommit.commit.message || '',
+      author:
+        latestCommit.commit.author?.name ||
+        latestCommit.commit.committer?.name ||
+        '',
+    }
+  } catch (error) {
+    console.error(`Error fetching latest commit for ${owner}/${repo}:`, error)
+    return null
+  }
+}
+
+/**
+ * Gets organization information for a repository
+ * @returns Organization avatar URL or null
+ */
+export const getOrganizationInfo = async (
+  installationId: number,
+  owner: string,
+  repo: string,
+): Promise<{ avatar_url: string } | null> => {
+  const octokit = await createOctokit(installationId)
+
+  try {
+    const { data } = await octokit.repos.get({
+      owner,
+      repo,
+    })
+
+    return {
+      avatar_url: data.organization?.avatar_url || '',
+    }
+  } catch (error) {
+    console.error(
+      `Error fetching organization info for ${owner}/${repo}:`,
+      error,
+    )
+    return null
+  }
+}


Reviews:
user: login: FunamaYukina
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T05:52:53Z
user: login: junkisai
body: I left one comment. 🙏 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T06:42:28Z
user: login: FunamaYukina
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T11:51:04Z
user: login: FunamaYukina
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T12:40:33Z
user: login: FunamaYukina
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T02:57:16Z
user: login: FunamaYukina
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T03:19:31Z
user: login: FunamaYukina
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T03:45:46Z
user: login: junkisai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:01:25Z
user: login: junkisai
body: Thank you for the adjustment!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:02:54Z
user: login: FunamaYukina
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T07:20:27Z

Review comments:
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: junkisai
body: Would it be difficult to fetch this data on the server side?
If this part can be executed on the server, I think we might not need to make ProjectItem a client component.
created_at: 2025-04-17T06:42:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: FunamaYukina
body: I will check.🙏
created_at: 2025-04-17T11:51:04Z
author_association: MEMBER
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: FunamaYukina
body: I was able to fix it, but now I'm getting the console error again, so I'm going to Draft status.🙏
![ss 3158](https://github.com/user-attachments/assets/e0600556-676c-46e0-a2e8-34f36732c65e)

created_at: 2025-04-17T12:40:33Z
author_association: MEMBER
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: FunamaYukina
body: Fixed to not fetch data directly from `ProjectItem.tsx`. The following structure is used to fetch data in `LastCommitDataWrapper`, but please let me know if I am wrong.🙏

```
[Client Component]                    [Server Action]
ProjectItem                             
    ↓                                  
LastCommitDataWrapper ----------------→ fetchLastCommitData
    • Client-side state management        • Server-side data fetching  
    • Data fetching inside useEffect      • Fetching commit info from GitHub  
    • Loading state handling              • Error handling  

```
created_at: 2025-04-18T02:57:16Z
author_association: MEMBER
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: FunamaYukina
body: Sorry, the user name is no longer displayed, I'll fix it.
![ss 3160](https://github.com/user-attachments/assets/12405968-4bfa-44da-adc4-4a602fb0dc4b)

created_at: 2025-04-18T03:19:31Z
author_association: MEMBER
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: FunamaYukina
body: I fixed it.🙏
https://github.com/liam-hq/liam/pull/1376/commits/76d8c6ccd4aec194c9dd68f811cdeb8676086dad
created_at: 2025-04-18T03:45:46Z
author_association: MEMBER
diff_hunk: @@ -26,6 +42,42 @@ export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
     })
   }
 
+  const repositoryName = project.name?.toLowerCase() || 'untitled-project'
+  const repository = project.ProjectRepositoryMapping?.[0]?.repository
+
+  // Fetch organization and commit data
+  useEffect(() => {
+    if (repository) {
+      const fetchData = async () => {
+        setIsLoading(true)
+
+        try {
+          // Fetch organization data
+          const orgData = await getOrganizationData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setAvatarUrl(orgData)
+
+          // Fetch commit data
+          const commitData = await getLastCommitData({
+            installationId: repository.installationId,
+            owner: repository.owner,
+            repo: repository.name,
+          })
+          setCommitInfo(commitData)
+        } catch (error) {
+          console.error('Error fetching data:', error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      fetchData()
+    }
path: frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx
user: login: junkisai
body: I see, so `ClientSearchWrapper`, which is calling `ProjectItem`, is a client component.
created_at: 2025-04-18T07:01:24Z
author_association: CONTRIBUTOR
path: frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx
user: login: junkisai
body: This might be an unused component. 👀  

created_at: 2025-04-18T07:02:11Z
author_association: CONTRIBUTOR
path: frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx
user: login: FunamaYukina
body: Oh, Thanks for letting me know!🙏 I will fix it and merge this PR!
created_at: 2025-04-18T07:20:26Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-16T12:53:08Z
body: ###  ⚠️  No Changeset found

Latest commit: 67c5cece34733f81b5d45490b047466be5984d56

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/liam-hq/liam/new/devin/1744807573-project-last-commit-time?filename=.changeset/good-elephants-love.md&value=---%0A%22%40liam-hq%2Fapp%22%3A%20patch%0A%22%40liam-hq%2Fgithub%22%3A%20patch%0A---%0A%0A%E2%9C%A8%20Display%20last%20repository%20commit%20time%20and%20organization%20icon%20in%20ProjectItem%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-16T12:53:08Z
body: [vc]: #QpWbDu+K5+3wrp4ddhfbqic1neHwL+G+3trkEHAHj3c=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJsaWFtLWRvY3MiLCJyb290RGlyZWN0b3J5IjoiZnJvbnRlbmQvYXBwcy9kb2NzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm91dGUtMDYtY29yZS9saWFtLWRvY3MvRnpYR0drUXpza2dMV0g0UlBrdDlrdkdxanJTdSIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6ImxpYW0tZG9jcy1naXQtZGV2aW4tMTc0NDgwNzU3My1wcm9qZWN0LWxhcy00YWEyM2Mtcm91dGUtMDYtY29yZS52ZXJjZWwuYXBwIn0seyJuYW1lIjoibGlhbS1lcmQtc2FtcGxlIiwicm9vdERpcmVjdG9yeSI6ImZyb250ZW5kL2FwcHMvZXJkLXNhbXBsZSIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9yb3V0ZS0wNi1jb3JlL2xpYW0tZXJkLXNhbXBsZS8yaG5oVzJFU1d5OUtCVjY5dktmbzNjTjlxWXIxIiwicHJldmlld1VybCI6ImxpYW0tZXJkLXNhbXBsZS1naXQtZGV2aW4tMTc0NDgwNzU3My1wcm9qZS00OGYxMGUtcm91dGUtMDYtY29yZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImxpYW0tZXJkLXNhbXBsZS1naXQtZGV2aW4tMTc0NDgwNzU3My1wcm9qZS00OGYxMGUtcm91dGUtMDYtY29yZS52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImxpYW0tYXBwIiwicm9vdERpcmVjdG9yeSI6ImZyb250ZW5kL2FwcHMvYXBwIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3JvdXRlLTA2LWNvcmUvbGlhbS1hcHAvNU51cVVrY1F5dGNVcUtoc2Y0aHRWbVFMY21NYSIsInByZXZpZXdVcmwiOiJsaWFtLWFwcC1naXQtZGV2aW4tMTc0NDgwNzU3My1wcm9qZWN0LWxhc3QtNzEzZGJlLXJvdXRlLTA2LWNvcmUudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJsaWFtLWFwcC1naXQtZGV2aW4tMTc0NDgwNzU3My1wcm9qZWN0LWxhc3QtNzEzZGJlLXJvdXRlLTA2LWNvcmUudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **liam-app** | ✅ Ready ([Inspect](https://vercel.com/route-06-core/liam-app/5NuqUkcQytcUqKhsf4htVmQLcmMa)) | [Visit Preview](https://liam-app-git-devin-1744807573-project-last-713dbe-route-06-core.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/liam-app-git-devin-1744807573-project-last-713dbe-route-06-core.vercel.app?via=pr-comment-feedback-link) | Apr 18, 2025 7:25am |
| **liam-erd-sample** | ✅ Ready ([Inspect](https://vercel.com/route-06-core/liam-erd-sample/2hnhW2ESWy9KBV69vKfo3cN9qYr1)) | [Visit Preview](https://liam-erd-sample-git-devin-1744807573-proje-48f10e-route-06-core.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/liam-erd-sample-git-devin-1744807573-proje-48f10e-route-06-core.vercel.app?via=pr-comment-feedback-link) | Apr 18, 2025 7:25am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **liam-docs** | ⬜️ Ignored ([Inspect](https://vercel.com/route-06-core/liam-docs/FzXGGkQzskgLWH4RPkt9kvGqjrSu)) | [Visit Preview](https://liam-docs-git-devin-1744807573-project-las-4aa23c-route-06-core.vercel.app) |  | Apr 18, 2025 7:25am |
</details>




author_association: NONE
user: login: devin-ai-integration[bot]
created_at: 2025-04-16T12:53:10Z
body: ### 🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:
- Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
- Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:
- [x] Disable automatic comment and CI monitoring

author_association: CONTRIBUTOR
user: login: FunamaYukina
created_at: 2025-04-17T05:52:33Z
body: (aside)
I have made many changes of my own to accommodate console error. I would like someone to review them for me.🙏

To solve this problem to address the following error, we separate the client component from the server component.
```
OrganizationIcon.tsx:13 Cannot update a component (`Router`) while rendering a different component (`OrganizationIcon`). 
To locate the bad setState() call inside `OrganizationIcon`, follow the stack trace as described in https://react.dev/link/setstate-in-render
```

Like this:

LastCommitInfo.tsx (server component) 
  ↓ Get data
LastCommitInfoClient.tsx (client component)
  ↓ Display
ProjectItem.tsx (use data acquisition function in client component)
author_association: MEMBER
user: login: qodo-free-for-open-source-projects[bot]
created_at: 2025-04-17T05:52:53Z
body: ## PR Reviewer Guide 🔍

#### (Review updated until commit https://github.com/liam-hq/liam/commit/f391335b7b78dfa1b5f2c33f9d779ff85adc494f)


Here are some key observations to aid the review process:

<table>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 3 🔵🔵🔵⚪⚪</td></tr>
<tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/liam-hq/liam/pull/1376/files#diff-c3ff1c4332f8730f3107455b525877ded57f2fd1b0002ce45d86ee4f916fdf25R1-R48'><strong>Redundant Component</strong></a>

This component appears to be unused and duplicates functionality already implemented in LastCommitDataWrapper. It performs the same task of fetching and displaying commit information.
</summary>

```typescriptreact
import { getLastCommit } from '@liam-hq/github'

interface LastCommitInfoProps {
  installationId: number
  owner: string
  repo: string
  defaultDate: string
}

export async function LastCommitInfo({
  installationId,
  owner,
  repo,
  defaultDate,
}: LastCommitInfoProps) {
  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    return date.toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
      year: 'numeric',
    })
  }

  try {
    const commitInfo = await getLastCommit(installationId, owner, repo)

    if (commitInfo) {
      return (
        <>
          <span>{commitInfo.author}</span>
          <span>committed</span>
          <span>on {formatDate(commitInfo.date)}</span>
        </>
      )
    }
  } catch (error) {
    console.error('Failed to fetch last commit info:', error)
  }

  return (
    <>
      <span>User</span>
      <span>committed</span>
      <span>on {formatDate(defaultDate)}</span>
    </>
  )
}
```

</details>

<details><summary><a href='https://github.com/liam-hq/liam/pull/1376/files#diff-7f7be9ff37873d4be4767875384e70589faba580791f707eb9c4aee720844c12R53-R54'><strong>Loading State</strong></a>

The loading state displays "Loading commit info..." text which may cause layout shifts when data loads. Consider using a skeleton loader or maintaining consistent layout during loading.
</summary>

```typescriptreact
  return <span>Loading commit info...</span>
}
```

</details>

<details><summary><a href='https://github.com/liam-hq/liam/pull/1376/files#diff-03baff0e4f9093a3b8da54dd071510aa745edbdbdae9e5d0c69eb3eaaff2e079R71-R75'><strong>Null Safety</strong></a>

When accessing repository properties like installation_id, owner, and name, there's no null check for the repository object itself, which could lead to runtime errors if repository is undefined.
</summary>

```typescriptreact
<LastCommitDataWrapper
  installationId={repository.installation_id}
  owner={repository.owner}
  repo={repository.name}
  defaultDate={project.created_at}
```

</details>

</td></tr>
</table>

author_association: CONTRIBUTOR
user: login: qodo-free-for-open-source-projects[bot]
created_at: 2025-04-17T05:53:45Z
body: ## PR Code Suggestions ✨
<!-- f391335 -->

Latest suggestions up to f391335

Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=2>General</td>
<td>



<details><summary>✅ 
Remove duplicate component file
</summary>

___

<details><summary><b>Suggestion Impact:</b></summary>The entire LastCommitInfo.tsx file was removed as suggested, eliminating the duplicate functionality and reducing maintenance overhead

code diff:

```diff
-import { getLastCommit } from '@liam-hq/github'
 
-interface LastCommitInfoProps {
-  installationId: number
-  owner: string
-  repo: string
-  defaultDate: string
-}
-
-export async function LastCommitInfo({
-  installationId,
-  owner,
-  repo,
-  defaultDate,
-}: LastCommitInfoProps) {
-  const formatDate = (dateString: string) => {
-    const date = new Date(dateString)
-    return date.toLocaleDateString('en-US', {
-      month: 'short',
-      day: 'numeric',
-      year: 'numeric',
-    })
-  }
-
-  try {
-    const commitInfo = await getLastCommit(installationId, owner, repo)
-
-    if (commitInfo) {
-      return (
-        <>
-          <span>{commitInfo.author}</span>
-          <span>committed</span>
-          <span>on {formatDate(commitInfo.date)}</span>
-        </>
-      )
-    }
-  } catch (error) {
-    console.error('Failed to fetch last commit info:', error)
-  }
-
-  return (
-    <>
-      <span>User</span>
-      <span>committed</span>
-      <span>on {formatDate(defaultDate)}</span>
-    </>
-  )
-}
```

</details>


___


**The <code>LastCommitInfo.tsx</code> file appears to be unused in the PR but duplicates <br>functionality already implemented in <code>LastCommitData.tsx</code> and <br><code>LastCommitDataWrapper.tsx</code>. This creates maintenance overhead and potential <br>confusion. Consider removing this file to avoid duplication.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [1-8]](https://github.com/liam-hq/liam/pull/1376/files#diff-c3ff1c4332f8730f3107455b525877ded57f2fd1b0002ce45d86ee4f916fdf25R1-R8)

```diff
-import { getLastCommit } from '@liam-hq/github'
+// This file should be removed as it duplicates functionality in LastCommitData.tsx and LastCommitDataWrapper.tsx
 
-interface LastCommitInfoProps {
-  installationId: number
-  owner: string
-  repo: string
-  defaultDate: string
-}
-
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: The LastCommitInfo.tsx file duplicates functionality already implemented in LastCommitData.tsx and LastCommitDataWrapper.tsx, creating unnecessary maintenance overhead and potential confusion in the codebase.


</details></details></td><td align=center>Medium

</td></tr><tr><td>



<details><summary>Improve loading state consistency</summary>

___


**The loading state displays a text that might disrupt the UI layout. Consider <br>using a more subtle loading indicator or maintaining the same structure as the <br>loaded state to prevent layout shifts.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitDataWrapper.tsx [52-54]](https://github.com/liam-hq/liam/pull/1376/files#diff-7f7be9ff37873d4be4767875384e70589faba580791f707eb9c4aee720844c12R52-R54)

```diff
 if (isLoading) {
-  return <span>Loading commit info...</span>
+  return (
+    <>
+      <span>Loading</span>
+      <span>commit</span>
+      <span>info...</span>
+    </>
+  )
 }
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=1 -->

<details><summary>Suggestion importance[1-10]: 5</summary>

__

Why: The suggestion improves UI consistency by maintaining the same structure during loading as in the loaded state, which prevents layout shifts and provides a better user experience.


</details></details></td><td align=center>Low

</td></tr><tr><td rowspan=1>Possible issue</td>
<td>



<details><summary>Add image error handling</summary>

___


**The <code>Image</code> component is missing error handling. If the image fails to load, it <br>will silently fall back to the default Next.js error display. Add an <code>onError</code> <br>handler to gracefully fall back to the <code>ProjectIcon</code> component.**

[frontend/apps/app/features/projects/components/ProjectItem/OrganizationIcon.tsx [12-26]](https://github.com/liam-hq/liam/pull/1376/files#diff-fba9a5c233dff417bca5d48aa16e8798c46f9842350b4f088b616b011c4490d6R12-R26)

```diff
 export function OrganizationIcon({ avatarUrl, owner }: OrganizationIconProps) {
-  if (avatarUrl) {
+  const [hasError, setHasError] = useState(false);
+  
+  if (avatarUrl && !hasError) {
     return (
       <Image
         src={avatarUrl}
         alt={`${owner} organization icon`}
         width={32}
         height={32}
         className={styles.projectIcon}
+        onError={() => setHasError(true)}
       />
     )
   }
 
   return <ProjectIcon className={styles.projectIcon} />
 }
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=2 -->

<details><summary>Suggestion importance[1-10]: 7</summary>

__

Why: Adding error handling for the Image component is important to gracefully handle cases where GitHub avatar images fail to load, preventing unexpected UI behavior and providing a consistent fallback.


</details></details></td><td align=center>Medium

</td></tr><tr><td rowspan=1>Learned<br>best practice</td>
<td>



<details><summary>Add stronger input validation with type checking and format validation for function parameters to prevent potential runtime errors</summary>

___


**The current validation only checks if the parameters exist but doesn't validate <br>their types or formats. For <code>installationId</code>, we should verify it's a positive <br>number, and for <code>owner</code> and <code>repo</code>, we should check they are non-empty strings with <br>valid GitHub repository naming patterns.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitData.tsx [6-29]](https://github.com/liam-hq/liam/pull/1376/files#diff-7e8f9ea7c592f4de7cfb268dffbe96072cbc65a44923c1f80aec2d2a2f7631d3R6-R29)

```diff
 export async function fetchLastCommitData(
   installationId: number,
   owner: string,
   repo: string,
 ) {
   try {
-    if (!installationId || !owner || !repo) {
+    // Validate installation ID is a positive number
+    if (!installationId || typeof installationId !== 'number' || installationId <= 0) {
+      console.error('Invalid installation ID:', installationId)
+      return null
+    }
+    
+    // Validate owner and repo are non-empty strings with valid GitHub naming patterns
+    if (!owner || typeof owner !== 'string' || !/^[a-zA-Z0-9-]+$/.test(owner)) {
+      console.error('Invalid owner name:', owner)
+      return null
+    }
+    
+    if (!repo || typeof repo !== 'string' || !/^[a-zA-Z0-9._-]+$/.test(repo)) {
+      console.error('Invalid repository name:', repo)
       return null
     }
 
     const commitInfo = await getLastCommit(installationId, owner, repo)
 
     if (commitInfo) {
       return {
         author: commitInfo.author,
         date: commitInfo.date,
       }
     }
 
     return null
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
     return null
   }
 }
```


`[To ensure code accuracy, apply this suggestion manually]`

<details><summary>Suggestion importance[1-10]: 6</summary>

</details></details></td><td align=center>Low

</td></tr>
<tr><td align="center" colspan="2">

- [ ] Update <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>

___

#### Previous suggestions


<details><summary>✅ Suggestions up to commit 3ee5ddc</summary>
<br><table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>Possible issue</td>
<td>



<details><summary>✅ 
Prevent state updates after unmount
</summary>

___

<details><summary><b>Suggestion Impact:</b></summary>The commit completely refactored the component to move the data fetching logic out of the component, eliminating the need for the suggested pattern. Instead of implementing the suggested cleanup pattern, the commit solved the underlying problem by moving the async operations to child components.

code diff:

```diff
-import { useEffect, useState } from 'react'
 import type { FC } from 'react'
-import { getLastCommitData } from './LastCommitInfo'
-import { LastCommitInfoClient } from './LastCommitInfoClient'
-import { getOrganizationData } from './OrganizationIcon'
-import { OrganizationIconClient } from './OrganizationIconClient'
+import { LastCommitInfo } from './LastCommitInfo'
+import { OrganizationIcon } from './OrganizationIcon'
 import { ProjectIcon } from './ProjectIcon'
 import styles from './ProjectItem.module.css'
 
@@ -24,14 +19,6 @@
 }
 
 export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
-  // State for organization avatar and commit info
-  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
-  const [commitInfo, setCommitInfo] = useState<{
-    author: string
-    date: string
-  } | null>(null)
-  const [isLoading, setIsLoading] = useState(true)
-
   // Format date to "MMM DD, YYYY" format
   const formatDate = (dateString: string) => {
     const date = new Date(dateString)
@@ -45,39 +32,6 @@
   const repositoryName = project.name?.toLowerCase() || 'untitled-project'
   const repository = project.ProjectRepositoryMapping?.[0]?.repository
 
-  // Fetch organization and commit data
-  useEffect(() => {
-    if (repository) {
-      const fetchData = async () => {
-        setIsLoading(true)
-
-        try {
-          // Fetch organization data
-          const orgData = await getOrganizationData({
-            installationId: repository.installationId,
-            owner: repository.owner,
-            repo: repository.name,
-          })
-          setAvatarUrl(orgData)
-
-          // Fetch commit data
-          const commitData = await getLastCommitData({
-            installationId: repository.installationId,
-            owner: repository.owner,
-            repo: repository.name,
-          })
-          setCommitInfo(commitData)
-        } catch (error) {
-          console.error('Error fetching data:', error)
-        } finally {
-          setIsLoading(false)
-        }
-      }
-
-      fetchData()
-    }
-  }, [repository])
```

</details>


___


**The <code>fetchData</code> function is defined inside a <code>useEffect</code> hook without being wrapped <br>in <code>useCallback</code>, which could lead to unnecessary re-renders. Additionally, <br>there's no cleanup function to handle component unmounting during async <br>operations, which could cause memory leaks or state updates on unmounted <br>components.**

[frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx [48-79]](https://github.com/liam-hq/liam/pull/1376/files#diff-03baff0e4f9093a3b8da54dd071510aa745edbdbdae9e5d0c69eb3eaaff2e079R48-R79)

```diff
 // Fetch organization and commit data
 useEffect(() => {
+  let isMounted = true;
+  
   if (repository) {
     const fetchData = async () => {
       setIsLoading(true)
 
       try {
         // Fetch organization data
         const orgData = await getOrganizationData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
-        setAvatarUrl(orgData)
+        if (isMounted) setAvatarUrl(orgData)
 
         // Fetch commit data
         const commitData = await getLastCommitData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
-        setCommitInfo(commitData)
+        if (isMounted) setCommitInfo(commitData)
       } catch (error) {
         console.error('Error fetching data:', error)
       } finally {
-        setIsLoading(false)
+        if (isMounted) setIsLoading(false)
       }
     }
 
     fetchData()
   }
+  
+  return () => {
+    isMounted = false;
+  }
 }, [repository])
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: This suggestion addresses an important React pattern to prevent memory leaks by adding cleanup logic that prevents state updates after component unmounting. This is a significant improvement that helps avoid potential bugs when asynchronous operations complete after a component is no longer mounted.


</details></details></td><td align=center>Medium

</td></tr><tr><td rowspan=1>General</td>
<td>



<details><summary>Eliminate duplicate API call<!-- not_implemented --></summary>

___


**The function duplicates logic already present in <code>getLastCommitData()</code>. Consider <br>reusing that function instead of directly calling <code>getLastCommit()</code> again, which <br>would reduce code duplication and ensure consistent behavior between the two <br>functions.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]](https://github.com/liam-hq/liam/pull/1376/files#diff-c3ff1c4332f8730f3107455b525877ded57f2fd1b0002ce45d86ee4f916fdf25R34-R56)

```diff
 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
   try {
-    const commitInfo = await getLastCommit(installationId, owner, repo)
+    const commitData = await getLastCommitData({ installationId, owner, repo })
 
-    if (commitInfo) {
+    if (commitData) {
       return (
         <LastCommitInfoClient
-          author={commitInfo.author}
-          date={commitInfo.date}
+          author={commitData.author}
+          date={commitData.date}
         />
       )
     }
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
   }
 
   return <LastCommitInfoClient date={defaultDate} />
 }
```

 <!-- /improve --apply_suggestion=1 -->

<details><summary>Suggestion importance[1-10]: 7</summary>

__

Why: The suggestion correctly identifies code duplication where LastCommitInfo directly calls getLastCommit instead of reusing the existing getLastCommitData function. This refactoring would improve maintainability and ensure consistent behavior between the two functions.


</details></details></td><td align=center>Medium

</td></tr><tr><td rowspan=1>Learned<br>best practice</td>
<td>



<details><summary>Add input validation for required parameters before processing to prevent potential runtime errors<!-- not_implemented --></summary>

___


**Add validation for required parameters at the beginning of the function to <br>prevent potential runtime errors. The function should validate that <br><code>installationId</code>, <code>owner</code>, and <code>repo</code> are valid values before attempting to fetch <br>commit information.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]](https://github.com/liam-hq/liam/pull/1376/files#diff-c3ff1c4332f8730f3107455b525877ded57f2fd1b0002ce45d86ee4f916fdf25R34-R56)

```diff
 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
+  if (!installationId || !owner || !repo) {
+    console.error('Missing required parameters for LastCommitInfo');
+    return <LastCommitInfoClient date={defaultDate} />;
+  }
+  
   try {
     const commitInfo = await getLastCommit(installationId, owner, repo)
 
     if (commitInfo) {
       return (
         <LastCommitInfoClient
           author={commitInfo.author}
           date={commitInfo.date}
         />
       )
     }
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
   }
 
   return <LastCommitInfoClient date={defaultDate} />
 }
```

 <!-- /improve --apply_suggestion=2 -->

<details><summary>Suggestion importance[1-10]: 6</summary>

</details></details></td><td align=center>Low

</td></tr>
<tr><td align="center" colspan="2">

 <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>

</details>
<details><summary>Suggestions up to commit 3ee5ddc</summary>
<br><table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>General</td>
<td>



<details><summary>Eliminate duplicate code<!-- not_implemented --></summary>

___


**The function duplicates logic that already exists in <code>getLastCommitData()</code>. You <br>should reuse the existing function to avoid code duplication and ensure <br>consistent behavior.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]](https://github.com/liam-hq/liam/pull/1376/files#diff-c3ff1c4332f8730f3107455b525877ded57f2fd1b0002ce45d86ee4f916fdf25R34-R56)

```diff
 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
-  try {
-    const commitInfo = await getLastCommit(installationId, owner, repo)
-
-    if (commitInfo) {
-      return (
-        <LastCommitInfoClient
-          author={commitInfo.author}
-          date={commitInfo.date}
-        />
-      )
-    }
-  } catch (error) {
-    console.error('Failed to fetch last commit info:', error)
-  }
-
-  return <LastCommitInfoClient date={defaultDate} />
+  const commitInfo = await getLastCommitData({ installationId, owner, repo });
+  
+  return (
+    <LastCommitInfoClient
+      author={commitInfo?.author}
+      date={commitInfo?.date || defaultDate}
+    />
+  );
 }
```

 <!-- /improve --apply_suggestion=0 -->

<details><summary>Suggestion importance[1-10]: 7</summary>

__

Why: The suggestion correctly identifies code duplication between LastCommitInfo and getLastCommitData functions. Refactoring to reuse the existing function improves maintainability and reduces the chance of inconsistent behavior between the two implementations.


</details></details></td><td align=center>Medium

</td></tr><tr><td rowspan=1>Learned<br>best practice</td>
<td>



<details><summary>Add input validation to check for required parameters before making API calls to prevent runtime errors<!-- not_implemented --></summary>

___


**Add input validation at the beginning of the function to check if required <br>parameters are present and valid before making the API call. This prevents <br>unnecessary API calls and potential runtime errors when parameters are missing <br>or invalid.**

[frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]](https://github.com/liam-hq/liam/pull/1376/files#diff-c3ff1c4332f8730f3107455b525877ded57f2fd1b0002ce45d86ee4f916fdf25R34-R56)

```diff
 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
+  // Validate required parameters
+  if (!installationId || !owner || !repo) {
+    console.error('Missing required parameters for LastCommitInfo')
+    return <LastCommitInfoClient date={defaultDate} />
+  }
+  
   try {
     const commitInfo = await getLastCommit(installationId, owner, repo)
 
     if (commitInfo) {
       return (
         <LastCommitInfoClient
           author={commitInfo.author}
           date={commitInfo.date}
         />
       )
     }
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
   }
 
   return <LastCommitInfoClient date={defaultDate} />
 }
```

 <!-- /improve --apply_suggestion=1 -->

<details><summary>Suggestion importance[1-10]: 6</summary>

</details></details></td><td align=center>Low

</td></tr><tr><td rowspan=1>Possible issue</td>
<td>



<details><summary>Fix effect dependency<!-- not_implemented --></summary>

___


**The effect doesn't handle the case when <code>repository</code> is undefined but later <br>becomes defined. Add a condition to also run the effect when <code>isLoading</code> is true <br>to ensure data is fetched when repository becomes available.**

[frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx [49-79]](https://github.com/liam-hq/liam/pull/1376/files#diff-03baff0e4f9093a3b8da54dd071510aa745edbdbdae9e5d0c69eb3eaaff2e079R49-R79)

```diff
 useEffect(() => {
   if (repository) {
     const fetchData = async () => {
       setIsLoading(true)
 
       try {
         // Fetch organization data
         const orgData = await getOrganizationData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
         setAvatarUrl(orgData)
 
         // Fetch commit data
         const commitData = await getLastCommitData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
         setCommitInfo(commitData)
       } catch (error) {
         console.error('Error fetching data:', error)
       } finally {
         setIsLoading(false)
       }
     }
 
     fetchData()
   }
-}, [repository])
+}, [repository, isLoading])
```

 <!-- /improve --apply_suggestion=2 -->

<details><summary>Suggestion importance[1-10]: 3</summary>

__

Why: Adding isLoading to the dependency array could create an infinite loop since isLoading is set within the effect itself. This would cause the effect to run again whenever isLoading changes, which happens in every execution of the effect.


</details></details></td><td align=center>Low

</td></tr>
<tr><td align="center" colspan="2">

 <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>

</details>
author_association: CONTRIBUTOR
user: login: liam-migration-preview[bot]
created_at: 2025-04-18T02:10:52Z
body: This pull request refactors the database schema by renaming tables, columns, and indexes to snake_case with pluralized names and updates foreign keys accordingly. The key concern is ensuring robust rollback and data integrity during these atomic migration operations. Overall, the changes align with project rules and enhance maintainability and security while warranting thorough testing of performance and RLS impacts.

Migration URL: https://liam-app-git-staging-route-06-core.vercel.app/app/projects/9d777f64-400a-42f3-a60e-98a59fc97279/ref/devin%2F1744807573-project-last-commit-time/migrations/58440237-7d77-44e1-b8ee-5e023ac6117a

ER Diagram:
- View ERD for frontend/packages/db/schema/schema.sql: https://liam-app-git-staging-route-06-core.vercel.app/app/projects/9d777f64-400a-42f3-a60e-98a59fc97279/ref/devin%2F1744807573-project-last-commit-time/schema/frontend/packages/db/schema/schema.sql
author_association: NONE