diff --git a/apps/web/app/(public)/(projects)/projects/[id]/markdown-content.tsx b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/markdown-content.tsx similarity index 100% rename from apps/web/app/(public)/(projects)/projects/[id]/markdown-content.tsx rename to apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/markdown-content.tsx diff --git a/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/page.tsx b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/page.tsx new file mode 100644 index 00000000..dcf74980 --- /dev/null +++ b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/page.tsx @@ -0,0 +1,11 @@ +import ProjectPage from './project-page'; + +export default async function Page({ + params, +}: { + params: Promise<{ provider: string; org: string; repo: string }>; +}) { + const { provider, org, repo } = await params; + + return ; +} diff --git a/apps/web/app/(public)/(projects)/projects/[id]/project-description.tsx b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/project-description.tsx similarity index 96% rename from apps/web/app/(public)/(projects)/projects/[id]/project-description.tsx rename to apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/project-description.tsx index d41eb749..a8595227 100644 --- a/apps/web/app/(public)/(projects)/projects/[id]/project-description.tsx +++ b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/project-description.tsx @@ -13,10 +13,12 @@ export default function ProjectDescription({ repo, project, isOwner, + isNotInDatabase, }: { repo: ProjectData; project: ProjectWithRelations; isOwner: boolean; + isNotInDatabase: boolean; }) { const getAvatarImage = (): string => { if (repo && repo.owner && typeof repo.owner.avatar_url === 'string') { @@ -41,12 +43,14 @@ export default function ProjectDescription({ repoOwnerName={repo?.owner?.name || repo?.namespace?.name || ''} className="h-12 w-12 flex-shrink-0 rounded-none md:h-16 md:w-16" /> +
- + {!isNotInDatabase && }
@@ -110,17 +114,19 @@ function StatusBadges({ project }: { project: ProjectWithRelations }) { function ProjectTitleAndTicks({ project, className, + isNotInDatabase, }: { project: ProjectWithRelations; className: string; + isNotInDatabase: boolean; }) { return (

{project?.name}

- + {!isNotInDatabase && }
- + {!isNotInDatabase && }
); } diff --git a/apps/web/app/(public)/(projects)/projects/[id]/project-page.tsx b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/project-page.tsx similarity index 81% rename from apps/web/app/(public)/(projects)/projects/[id]/project-page.tsx rename to apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/project-page.tsx index b296b076..32f1bd4e 100644 --- a/apps/web/app/(public)/(projects)/projects/[id]/project-page.tsx +++ b/apps/web/app/(public)/(projects)/[provider]/[org]/[repo]/project-page.tsx @@ -21,12 +21,12 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { ClaimProjectDialog } from '@/components/project/claim-project-dialog'; import { ContributorData, ProjectWithRelations } from '@workspace/api'; +import ProjectErrorPage from '../../../projects/project-error-page'; import { Separator } from '@workspace/ui/components/separator'; import { useQueries, useQuery } from '@tanstack/react-query'; import { projectProviderEnum } from '@workspace/db/schema'; import LoadingSpinner from '@/components/loading-spinner'; import ProjectDescription from './project-description'; -import ProjectErrorPage from '../project-error-page'; import { MarkdownContent } from './markdown-content'; import { authClient } from '@workspace/auth/client'; import { useEffect, useState, useRef } from 'react'; @@ -93,6 +93,17 @@ interface Repository { id?: string; name?: string; url?: string; + description?: string; + homepage?: string; + isPrivate?: boolean; + private?: boolean; + topics?: string[]; + owner?: { + avatar_url?: string; + }; + namespace?: { + avatar_url?: string; + }; } interface RepoData { @@ -101,53 +112,53 @@ interface RepoData { pullRequestsCount?: number; } -interface Project { - id: string; - ownerId: string | null; - logoUrl: string | null; - gitRepoUrl: string | null; - gitHost: string | null; - name: string; - description: string | null; - socialLinks: { - twitter?: string; - discord?: string; - linkedin?: string; - website?: string; - [key: string]: string | undefined; - } | null; - approvalStatus: 'pending' | 'approved' | 'rejected'; - isPinned: boolean; - hasBeenAcquired: boolean; - isLookingForContributors: boolean; - isLookingForInvestors: boolean; - isHiring: boolean; - isPublic: boolean; - isRepoPrivate: boolean; - acquiredBy: string | null; - createdAt: Date; - updatedAt: Date; - statusId: string; - typeId: string; - deletedAt: Date | null; - status?: { - id: string; - name: string; - displayName?: string; - }; - type?: { - id: string; - name: string; - displayName?: string; - }; - tagRelations?: Array<{ - tag?: { - id?: string; - name: string; - displayName?: string; - }; - }>; -} +// interface Project { +// id: string; +// ownerId: string | null; +// logoUrl: string | null; +// gitRepoUrl: string | null; +// gitHost: string | null; +// name: string; +// description: string | null; +// socialLinks: { +// twitter?: string; +// discord?: string; +// linkedin?: string; +// website?: string; +// [key: string]: string | undefined; +// } | null; +// approvalStatus: 'pending' | 'approved' | 'rejected'; +// isPinned: boolean; +// hasBeenAcquired: boolean; +// isLookingForContributors: boolean; +// isLookingForInvestors: boolean; +// isHiring: boolean; +// isPublic: boolean; +// isRepoPrivate: boolean; +// acquiredBy: string | null; +// createdAt: Date; +// updatedAt: Date; +// statusId: string; +// typeId: string; +// deletedAt: Date | null; +// status?: { +// id: string; +// name: string; +// displayName?: string; +// }; +// type?: { +// id: string; +// name: string; +// displayName?: string; +// }; +// tagRelations?: Array<{ +// tag?: { +// id?: string; +// name: string; +// displayName?: string; +// }; +// }>; +// } const isValidProvider = ( provider: string | null | undefined, @@ -155,19 +166,35 @@ const isValidProvider = ( return provider === 'github' || provider === 'gitlab'; }; -function useProject(id: string) { +function useProject(provider: string, org: string, repo: string) { const trpc = useTRPC(); - const query = useQuery(trpc.projects.getProject.queryOptions({ id }, { retry: false })); + const query = useQuery( + trpc.projects.getProjectByRepo.queryOptions({ provider, org, repo }, { retry: false }), + ); return { - project: query.data as ProjectWithRelations | undefined, + project: query.data as ProjectWithRelations | null | undefined, isLoading: query.isLoading, error: query.error, + isNotInDatabase: query.data === null, }; } -export default function ProjectPage({ id }: { id: string }) { - const { project, isLoading: projectLoading, error: projectError } = useProject(id); +export default function ProjectPage({ + provider, + org, + repoId, +}: { + provider: string; + org: string; + repoId: string; +}) { + const { + project, + isLoading: projectLoading, + error: projectError, + isNotInDatabase, + } = useProject(provider, org, repoId); const { data: session } = authClient.useSession(); const user = session?.user; const [showShadow, setShowShadow] = useState(false); @@ -183,7 +210,6 @@ export default function ProjectPage({ id }: { id: string }) { return () => window.removeEventListener('scroll', handleScroll); }, []); - // Scroll active tab into view on smaller screens useEffect(() => { if (tabsListRef.current) { const activeTabElement = tabsListRef.current.querySelector( @@ -209,14 +235,17 @@ export default function ProjectPage({ id }: { id: string }) { const trpc = useTRPC(); + const normalizedProvider = provider === 'gh' ? 'github' : provider === 'gl' ? 'gitlab' : provider; + const gitRepoUrl = `${org}/${repoId}`; + const repoQuery = useQuery( trpc.repository.getRepo.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: (!!project?.gitRepoUrl || isNotInDatabase) && isValidProvider(normalizedProvider), retry: false, }, ), @@ -225,11 +254,14 @@ export default function ProjectPage({ id }: { id: string }) { const repoDataQuery = useQuery( trpc.repository.getRepoData.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), @@ -239,61 +271,79 @@ export default function ProjectPage({ id }: { id: string }) { queries: [ trpc.repository.getIssues.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), trpc.repository.getPullRequests.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), trpc.repository.getReadme.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), trpc.repository.getContributing.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), trpc.repository.getCodeOfConduct.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), - trpc.projects.getContributors.queryOptions( + trpc.repository.getContributors.queryOptions( { - url: project?.gitRepoUrl as string, - provider: project?.gitHost as (typeof projectProviderEnum.enumValues)[number], + url: gitRepoUrl, + provider: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], }, { - enabled: !!repoQuery.data && !!project?.gitRepoUrl && isValidProvider(project?.gitHost), + enabled: + !!repoQuery.data && + (!!project?.gitRepoUrl || isNotInDatabase) && + isValidProvider(normalizedProvider), retry: false, }, ), @@ -308,14 +358,10 @@ export default function ProjectPage({ id }: { id: string }) { ); } - if (projectError || !project) { + if (projectError && !isNotInDatabase) { return ; } - if (!project.gitRepoUrl) { - return ; - } - if (repoQuery.isLoading) { return (
@@ -328,14 +374,58 @@ export default function ProjectPage({ id }: { id: string }) { return repoQuery.refetch()} />; } - const isUnclaimed = !project.ownerId; - const isOwner = user?.id === project.ownerId; const repoData = repoQuery.data as Repository; + + const projectData: ProjectWithRelations = isNotInDatabase + ? { + id: `virtual-${normalizedProvider}-${org}-${repoId}`, + ownerId: null, + logoUrl: repoData.owner?.avatar_url || repoData.namespace?.avatar_url || null, + gitRepoUrl: gitRepoUrl, + gitHost: normalizedProvider as (typeof projectProviderEnum.enumValues)[number], + name: repoData.name || repoId, + description: repoData.description || null, + socialLinks: repoData.homepage ? { website: repoData.homepage } : null, + approvalStatus: 'approved' as const, + isPinned: false, + hasBeenAcquired: false, + isLookingForContributors: + repoData.topics?.includes('help-wanted') || + repoData.topics?.includes('good-first-issue') || + false, + isLookingForInvestors: + repoData.topics?.includes('seeking-funding') || + repoData.topics?.includes('investment') || + false, + isHiring: repoData.topics?.includes('hiring') || repoData.topics?.includes('jobs') || false, + isPublic: true, + isRepoPrivate: repoData.isPrivate || repoData.private || false, + acquiredBy: null, + createdAt: repoData.created_at ? new Date(repoData.created_at) : new Date(), + updatedAt: repoData.updated_at ? new Date(repoData.updated_at) : new Date(), + statusId: '', + typeId: '', + deletedAt: null, + status: undefined, + type: undefined, + tagRelations: + repoData.topics?.map((topic: string) => ({ + tag: { + id: topic, + name: topic, + displayName: topic, + }, + })) || [], + } + : project!; + + const isUnclaimed = !projectData.ownerId; + const isOwner = user?.id === projectData.ownerId; const repo = { ...repoData, id: repoData.html_url || repoData.web_url || '', - name: project.gitRepoUrl?.split('/').pop() || '', - url: repoData.html_url || repoData.web_url || project.gitRepoUrl || '', + name: projectData.gitRepoUrl?.split('/').pop() || '', + url: repoData.html_url || repoData.web_url || projectData.gitRepoUrl || '', }; const repoStats = repoDataQuery.data as RepoData | undefined; const issuesCount = repoStats?.issuesCount || 0; @@ -356,15 +446,26 @@ export default function ProjectPage({ id }: { id: string }) { />
- + {!isNotInDatabase ? ( + + ) : ( +
+

This repo has not yet been added to oss.now.

+
+ )}
- +
@@ -493,7 +594,7 @@ export default function ProjectPage({ id }: { id: string }) { @@ -550,17 +651,17 @@ export default function ProjectPage({ id }: { id: string }) { {issuesCount > 10 && ( View all {issuesCount} open issues on{' '} - {project?.gitHost === 'github' ? 'GitHub' : 'GitLab'} → + {projectData.gitHost === 'github' ? 'GitHub' : 'GitLab'} → )}
@@ -574,7 +675,7 @@ export default function ProjectPage({ id }: { id: string }) { {otherQueries[1].isLoading ? (
- +
) : pullRequests ? ( pullRequests.length === 0 ? ( @@ -621,7 +722,7 @@ export default function ProjectPage({ id }: { id: string }) { href={pr.html_url || pr.web_url || '#'} target="_blank" event="project_page_pull_request_link_clicked" - eventObject={{ projectId: project.id }} + eventObject={{ projectId: projectData.id }} className="mt-2 block text-sm font-medium text-neutral-300 transition-colors hover:text-white" > {pr.title} @@ -672,7 +773,7 @@ export default function ProjectPage({ id }: { id: string }) { @@ -684,17 +785,17 @@ export default function ProjectPage({ id }: { id: string }) { {pullRequestsCount > 10 && ( View all {pullRequestsCount} open pull requests on{' '} - {project?.gitHost === 'github' ? 'GitHub' : 'GitLab'} → + {projectData.gitHost === 'github' ? 'GitHub' : 'GitLab'} → )}
@@ -780,10 +881,13 @@ export default function ProjectPage({ id }: { id: string }) { = 500 @@ -902,7 +1006,7 @@ export default function ProjectPage({ id }: { id: string }) {

About

- {project?.createdAt && ( + {projectData?.createdAt && (
Created @@ -910,7 +1014,7 @@ export default function ProjectPage({ id }: { id: string }) {
)} - {project?.updatedAt && ( + {projectData?.updatedAt && (
Updated @@ -918,40 +1022,42 @@ export default function ProjectPage({ id }: { id: string }) {
)} - {project?.gitHost && ( + {projectData?.gitHost && (
Host - {project?.gitHost} + {projectData?.gitHost}
)}
Visibility - {project?.isPublic ? 'Public' : 'Private'} + {projectData?.isPublic ? 'Public' : 'Private'}
-
-

- - Tags -

-
- {project?.tagRelations?.map((relation, index: number) => ( - - {relation.tag?.displayName || relation.tag?.name} - - ))} + {!isNotInDatabase && ( +
+

+ + Tags +

+
+ {projectData?.tagRelations?.map((relation, index: number) => ( + + {relation.tag?.displayName || relation.tag?.name} + + ))} +
-
+ )} - {project?.hasBeenAcquired && project?.acquiredBy && ( + {projectData?.hasBeenAcquired && projectData?.acquiredBy && (
@@ -960,13 +1066,13 @@ export default function ProjectPage({ id }: { id: string }) {

This project has been acquired

)} - {(project?.isLookingForContributors || - project?.isLookingForInvestors || - project?.isHiring) && ( + {(projectData?.isLookingForContributors || + projectData?.isLookingForInvestors || + projectData?.isHiring) && (

Opportunities

- {project?.isLookingForContributors && ( + {projectData?.isLookingForContributors && (
@@ -977,7 +1083,7 @@ export default function ProjectPage({ id }: { id: string }) {
)} - {project?.isLookingForInvestors && ( + {projectData?.isLookingForInvestors && (
@@ -988,7 +1094,7 @@ export default function ProjectPage({ id }: { id: string }) {
)} - {project?.isHiring && ( + {projectData?.isHiring && (
diff --git a/apps/web/app/(public)/(projects)/projects/[id]/page.tsx b/apps/web/app/(public)/(projects)/projects/[id]/page.tsx deleted file mode 100644 index 9cac419f..00000000 --- a/apps/web/app/(public)/(projects)/projects/[id]/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import ProjectPage from './project-page'; - -export default async function Page({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - - return ; -} diff --git a/apps/web/app/(public)/(projects)/projects/project-card.tsx b/apps/web/app/(public)/(projects)/projects/project-card.tsx index dce97e04..70a77103 100644 --- a/apps/web/app/(public)/(projects)/projects/project-card.tsx +++ b/apps/web/app/(public)/(projects)/projects/project-card.tsx @@ -27,9 +27,14 @@ export default function ProjectCard({ project }: { project: Project }) { if (isError) return
Error
; + const normalizedProvider = + project.gitHost === 'github' ? 'gh' : project.gitHost === 'gitlab' ? 'gl' : project.gitHost; + + const href = `/${normalizedProvider}/${project.gitRepoUrl}`; + return ( )}
-
-

- {project.name} -

- {(project.isLookingForContributors || project.hasBeenAcquired) && ( -
- {project.isLookingForContributors && ( - - Open to contributors - - )} - {project.hasBeenAcquired && ( - - Acquired - - )} -
- )} +
+

+ {project.name} +

+ {(project.isLookingForContributors || project.hasBeenAcquired) && ( +
+ {project.isLookingForContributors && ( + + Open to contributors + + )} + {project.hasBeenAcquired && ( + + Acquired + + )} +
+ )}

{project.description} diff --git a/apps/web/app/(public)/launches/[id]/components/launch-sidebar.tsx b/apps/web/app/(public)/launches/[id]/components/launch-sidebar.tsx index 18411d3c..a3f436c2 100644 --- a/apps/web/app/(public)/launches/[id]/components/launch-sidebar.tsx +++ b/apps/web/app/(public)/launches/[id]/components/launch-sidebar.tsx @@ -9,16 +9,16 @@ import { Star, Users, } from 'lucide-react'; -import { Separator } from '@workspace/ui/components/separator'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Separator } from '@workspace/ui/components/separator'; import { projectProviderEnum } from '@workspace/db/schema'; import { Button } from '@workspace/ui/components/button'; +import { useRouter, usePathname } from 'next/navigation'; import { authClient } from '@workspace/auth/client'; import Link from '@workspace/ui/components/link'; import { formatDistanceToNow } from 'date-fns'; import { useTRPC } from '@/hooks/use-trpc'; import { toast } from 'sonner'; -import { useRouter, usePathname } from 'next/navigation'; const isValidProvider = ( provider: string | null | undefined, @@ -102,6 +102,9 @@ export default function LaunchSidebar({ launch, project, projectId }: LaunchSide const issuesCount = repoStats?.issuesCount || 0; const pullRequestsCount = repoStats?.pullRequestsCount || 0; + const provider = project.gitHost === 'github' ? 'gh' : 'gl'; + const href = `/${provider}/${repoData?.owner.login}/${repoData?.name}`; + return (

{/* Main Info Card */} @@ -216,7 +219,7 @@ export default function LaunchSidebar({ launch, project, projectId }: LaunchSide