diff --git a/app/(saas)/discover/_components/issue-card/issue-card.tsx b/app/(saas)/discover/_components/issue-card/issue-card.tsx
new file mode 100644
index 000000000..d42f22c2c
--- /dev/null
+++ b/app/(saas)/discover/_components/issue-card/issue-card.tsx
@@ -0,0 +1,88 @@
+import { useCallback } from "react";
+
+import { bootstrap } from "@/core/bootstrap";
+
+import { ContributionBadge } from "@/design-system/molecules/contribution-badge";
+
+import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar";
+import { Badge } from "@/shared/ui/badge";
+import { Card, CardTitle } from "@/shared/ui/card";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
+import { TypographyMuted, TypographySmall } from "@/shared/ui/typography";
+
+import { IssueCardProps } from "./issue-card.types";
+
+export function IssueCard({ title, project, languages, createdAt, labels, issue }: IssueCardProps) {
+ const limitedLabels = labels?.slice(0, 2) ?? [];
+
+ const dateKernelPort = bootstrap.getDateKernelPort();
+
+ const renderLanguages = useCallback(() => {
+ if (!languages?.length) return null;
+
+ return (
+
+
+
+ {languages.slice(0, 2).map(language => (
+
+
+ {language.name.charAt(0)}
+
+ ))}
+
+
+
+
+
+ {languages.map(language => (
+ -
+
+
+ {language.name.charAt(0)}
+
+ {language.name}
+
+ ))}
+
+
+
+ );
+ }, [languages]);
+
+ return (
+
+
+
+
+
+
+ {project.name.charAt(0)}
+
+
+
+ {project.name}/{project.repo}
+
+
+ {renderLanguages()}
+
+
+
+
+
+ );
+}
diff --git a/app/(saas)/discover/_components/issue-card/issue-card.types.ts b/app/(saas)/discover/_components/issue-card/issue-card.types.ts
new file mode 100644
index 000000000..f45c57e88
--- /dev/null
+++ b/app/(saas)/discover/_components/issue-card/issue-card.types.ts
@@ -0,0 +1,20 @@
+import { ContributionGithubStatusUnion } from "@/core/domain/contribution/models/contribution.types";
+
+export interface IssueCardProps {
+ title: string;
+ languages: {
+ name: string;
+ logoUrl: string;
+ }[];
+ project: {
+ logoUrl?: string;
+ name: string;
+ repo: string;
+ };
+ issue: {
+ number: number;
+ githubStatus: ContributionGithubStatusUnion;
+ };
+ createdAt: string;
+ labels: string[];
+}
diff --git a/app/(saas)/discover/_components/new-project-card/new-project-card.tsx b/app/(saas)/discover/_components/new-project-card/new-project-card.tsx
new file mode 100644
index 000000000..1bac25e47
--- /dev/null
+++ b/app/(saas)/discover/_components/new-project-card/new-project-card.tsx
@@ -0,0 +1,117 @@
+import { GitFork, Star, UserRound } from "lucide-react";
+import { useCallback } from "react";
+
+import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar";
+import { Badge } from "@/shared/ui/badge";
+import { Card, CardContent, CardFooter, CardTitle } from "@/shared/ui/card";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
+import { TypographyH4, TypographyMuted, TypographySmall } from "@/shared/ui/typography";
+import { cn } from "@/shared/utils";
+
+import { NewProjectCardProps } from "./new-project-card.types";
+
+function Metrics({ stars, forks, contributors }: { stars: number; forks: number; contributors: number }) {
+ return (
+
+
+
+
+ {Intl.NumberFormat().format(stars)}
+
+
+
+
+ {Intl.NumberFormat().format(forks)}
+
+
+
+
+ {Intl.NumberFormat().format(contributors)}
+
+
+ );
+}
+
+export function NewProjectCard({
+ name,
+ logoUrl,
+ description,
+ categories,
+ languages,
+ stars,
+ forks,
+ contributors,
+ className,
+}: NewProjectCardProps) {
+ const limitedCategories = categories?.slice(0, 2) ?? [];
+
+ const renderLanguages = useCallback(() => {
+ if (!languages?.length) return null;
+
+ return (
+
+
+
+ {languages.slice(0, 2).map(language => (
+
+
+ {language.name.charAt(0)}
+
+ ))}
+
+
+
+
+
+ {languages?.map(language => (
+ -
+
+
+
+ {language.name.charAt(0)}
+
+
{language.name}
+
+
+ {language.percentage}%
+
+ ))}
+
+
+
+ );
+ }, [languages]);
+
+ return (
+
+
+
+
+ {name.charAt(0)}
+
+
+
+
+
+ {description}
+
+
+
+
+ {limitedCategories.map(label => (
+
+ {label}
+
+ ))}
+
+
+ {renderLanguages()}
+
+
+ );
+}
diff --git a/app/(saas)/discover/_components/new-project-card/new-project-card.types.ts b/app/(saas)/discover/_components/new-project-card/new-project-card.types.ts
new file mode 100644
index 000000000..e32475eff
--- /dev/null
+++ b/app/(saas)/discover/_components/new-project-card/new-project-card.types.ts
@@ -0,0 +1,15 @@
+export interface NewProjectCardProps {
+ logoUrl: string;
+ name: string;
+ description: string;
+ categories: string[];
+ languages: {
+ name: string;
+ logoUrl: string;
+ percentage: number;
+ }[];
+ stars: number;
+ forks: number;
+ contributors: number;
+ className?: string;
+}
diff --git a/app/(saas)/discover/_components/page-banner/page-banner.tsx b/app/(saas)/discover/_components/page-banner/page-banner.tsx
new file mode 100644
index 000000000..f6cf78ac9
--- /dev/null
+++ b/app/(saas)/discover/_components/page-banner/page-banner.tsx
@@ -0,0 +1,58 @@
+import { Target } from "lucide-react";
+import Link from "next/link";
+import { useCallback } from "react";
+
+import { LiveHackathonCard } from "@/app/(saas)/osw/_components/live-hackathon-card/live-hackathon-card";
+
+import { HackathonReactQueryAdapter } from "@/core/application/react-query-adapter/hackathon";
+
+import { ErrorState } from "@/shared/components/error-state/error-state";
+import { NEXT_ROUTER } from "@/shared/constants/router";
+import { ListBanner } from "@/shared/features/list-banner/list-banner";
+import { Button } from "@/shared/ui/button";
+import { Skeleton } from "@/shared/ui/skeleton";
+
+export function PageBanner() {
+ const { data, isLoading, isError } = HackathonReactQueryAdapter.client.useGetHackathons({});
+
+ const renderLiveHackathon = useCallback(() => {
+ if (isLoading) {
+ return ;
+ }
+
+ if (isError) return ;
+
+ if (!data)
+ return (
+
+ Embark on an ODQuest Adventure
+ >
+ ),
+ }}
+ subtitle={{
+ children:
+ "Unlock epic rewards by conquering challenges and join a thriving community of adventurers on an exciting Quest!",
+ }}
+ logo={}
+ classNames={{
+ base: "bg-gradient-to-br from-indigo-900 to-transparent to-80%",
+ }}
+ >
+
+
+ );
+
+ const liveHackathon = data.hackathons.find(hackathon => hackathon.isLive());
+
+ if (!liveHackathon) return null;
+
+ return ;
+ }, [data, isError, isLoading]);
+
+ return renderLiveHackathon();
+}
diff --git a/app/(saas)/discover/_components/page-carousel/page-carousel.tsx b/app/(saas)/discover/_components/page-carousel/page-carousel.tsx
new file mode 100644
index 000000000..cd06ebc5b
--- /dev/null
+++ b/app/(saas)/discover/_components/page-carousel/page-carousel.tsx
@@ -0,0 +1,62 @@
+import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures";
+import { CircleDotDashed, Folder } from "lucide-react";
+import { Children, ReactElement } from "react";
+
+import { Badge } from "@/shared/ui/badge";
+import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/shared/ui/carousel";
+import { TypographyH3, TypographyMuted } from "@/shared/ui/typography";
+
+import { PageCarouselProps } from "./page-carousel.types";
+
+function ResourceBadge({ resourceType }: { resourceType: PageCarouselProps["resourceType"] }) {
+ const badgeLabel = resourceType === "issue" ? "Issues" : "Projects";
+ const badgeIcon =
+ resourceType === "issue" ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {badgeIcon}
+ {badgeLabel}
+
+ );
+}
+
+export function PageCarousel({ children, title, description, count, resourceType }: PageCarouselProps) {
+ const childrenArray = Children.toArray(children) as ReactElement[];
+
+ return (
+
+
+
+
+
+
+
+ {title}
+ {count && ` (${count})`}
+
+
+
{description}
+
+
+
+
+
+
+
+
+ {childrenArray.map((child, index) => (
+
+ {child}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/app/(saas)/discover/_components/page-carousel/page-carousel.types.ts b/app/(saas)/discover/_components/page-carousel/page-carousel.types.ts
new file mode 100644
index 000000000..e728658c5
--- /dev/null
+++ b/app/(saas)/discover/_components/page-carousel/page-carousel.types.ts
@@ -0,0 +1,9 @@
+import { ReactNode } from "react";
+
+export interface PageCarouselProps {
+ resourceType: "issue" | "project";
+ title: string;
+ description?: string;
+ count?: number;
+ children: ReactNode[];
+}
diff --git a/app/(saas)/discover/_features/page-header/page-header.tsx b/app/(saas)/discover/_features/page-header/page-header.tsx
new file mode 100644
index 000000000..7ff6093e1
--- /dev/null
+++ b/app/(saas)/discover/_features/page-header/page-header.tsx
@@ -0,0 +1,83 @@
+import background from "@/public/images/backgrounds/discover-header.png";
+import { ArrowRight, Bot, FolderSearch, LayoutList } from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
+
+import { NEXT_ROUTER } from "@/shared/constants/router";
+import { Button } from "@/shared/ui/button";
+import { Card, CardDescription, CardHeader, CardTitle } from "@/shared/ui/card";
+import { GlowingEffect } from "@/shared/ui/glowing-effect";
+import { TypographyH2, TypographyP } from "@/shared/ui/typography";
+
+function HasSufficentData() {
+ return (
+
+ );
+}
+
+function NoSufficentData() {
+ return (
+
+
Didn’t find what you’re looking for?
+
+
+
+
+
+
+
+
+ Browse
+
+ Explore projects on your own with Browse.
+
+
+
+
+
+
+
+
+
+
+ ODSay
+
+ Let our chatbot help you find the right project.
+
+
+
+
+
+ );
+}
+export function PageHeader({ hasSufficentData = false }: { hasSufficentData?: boolean }) {
+ const Footer = hasSufficentData ? HasSufficentData : NoSufficentData;
+
+ return (
+
+
+
+
+
+ Match your next
+
+ Open source contributions
+
+
+
+ Get recommendations based on your profile and past contributions
+
+
+
+
+
+ );
+}
diff --git a/app/(saas)/discover/_features/page-header/page-header.types.ts b/app/(saas)/discover/_features/page-header/page-header.types.ts
new file mode 100644
index 000000000..ba63136e5
--- /dev/null
+++ b/app/(saas)/discover/_features/page-header/page-header.types.ts
@@ -0,0 +1,3 @@
+import { PropsWithChildren } from "react";
+
+export interface PageHeaderProps extends PropsWithChildren {}
diff --git a/app/(saas)/discover/page.tsx b/app/(saas)/discover/page.tsx
index 717174e6e..5d10bfd8a 100644
--- a/app/(saas)/discover/page.tsx
+++ b/app/(saas)/discover/page.tsx
@@ -1,39 +1,7 @@
-import { Categories } from "@/app/(saas)/discover/_features/categories/categories";
-import { GoodFirstIssues } from "@/app/(saas)/discover/_features/good-first-issues/good-first-issues";
-import { MostCollaborative } from "@/app/(saas)/discover/_features/most-collaborative/most-collaborative";
-import { RecentActivity } from "@/app/(saas)/discover/_features/recent-activity/recent-activity";
-import { Trending } from "@/app/(saas)/discover/_features/trending/trending";
+"use client";
-import { NavigationBreadcrumb } from "@/shared/features/navigation/navigation.context";
-import { PageContainer } from "@/shared/features/page/page-container/page-container";
+import DiscoverPageV2 from "./v2";
export default function DiscoverPage() {
- return (
-
-
-
-
-
-
- {/*
*/}
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ return ;
}
diff --git a/app/(saas)/discover/v1.tsx b/app/(saas)/discover/v1.tsx
new file mode 100644
index 000000000..f19c7fcb7
--- /dev/null
+++ b/app/(saas)/discover/v1.tsx
@@ -0,0 +1,39 @@
+import { Categories } from "@/app/(saas)/discover/_features/categories/categories";
+import { GoodFirstIssues } from "@/app/(saas)/discover/_features/good-first-issues/good-first-issues";
+import { MostCollaborative } from "@/app/(saas)/discover/_features/most-collaborative/most-collaborative";
+import { RecentActivity } from "@/app/(saas)/discover/_features/recent-activity/recent-activity";
+import { Trending } from "@/app/(saas)/discover/_features/trending/trending";
+
+import { NavigationBreadcrumb } from "@/shared/features/navigation/navigation.context";
+import { PageContainer } from "@/shared/features/page/page-container/page-container";
+
+export default function DiscoverPageV1() {
+ return (
+
+
+
+
+
+
+ {/*
*/}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(saas)/discover/v2.tsx b/app/(saas)/discover/v2.tsx
new file mode 100644
index 000000000..9b35c69f2
--- /dev/null
+++ b/app/(saas)/discover/v2.tsx
@@ -0,0 +1,86 @@
+import { PageBanner } from "@/app/(saas)/discover/_components/page-banner/page-banner";
+
+import { RecoReactQueryAdapter } from "@/core/application/react-query-adapter/reco";
+
+import { NavigationBreadcrumb } from "@/shared/features/navigation/navigation.context";
+import { PageContainer } from "@/shared/features/page/page-container/page-container";
+import { PageInner } from "@/shared/features/page/page-inner/page-inner";
+
+import { IssueCard } from "./_components/issue-card/issue-card";
+import { NewProjectCard } from "./_components/new-project-card/new-project-card";
+import { PageCarousel } from "./_components/page-carousel/page-carousel";
+import { PageHeader } from "./_features/page-header/page-header";
+
+export default function DiscoverPageV2() {
+ const { data: tailoredDiscoveries } = RecoReactQueryAdapter.client.useGetTailoredDiscoveries({});
+
+ return (
+
+
+
+
+
+
+ {tailoredDiscoveries?.sections.map((section, index) => {
+ const resourceType = section.getResourceType();
+ const projects = section.getProjects();
+ const issues = section.getIssues();
+ const count = projects.length ?? issues.length ?? 0;
+
+ return (
+ <>
+
+ {projects.map(project => (
+ category.name)}
+ languages={project.languages}
+ stars={project.starCount}
+ forks={project.forkCount}
+ contributors={project.contributorCount}
+ />
+ ))}
+
+ {issues.map(issue => (
+ label.name)}
+ />
+ ))}
+
+
+ {index === 0 ? : null}
+ >
+ );
+ })}
+
+
+
+ );
+}
diff --git a/core/application/react-query-adapter/reco/client/index.ts b/core/application/react-query-adapter/reco/client/index.ts
index f2684ce50..356f27432 100644
--- a/core/application/react-query-adapter/reco/client/index.ts
+++ b/core/application/react-query-adapter/reco/client/index.ts
@@ -1,3 +1,4 @@
export * from "./use-get-matching-questions";
-export * from "./use-save-matching-questions";
export * from "./use-get-recommended-projects";
+export * from "./use-get-tailored-discoveries";
+export * from "./use-save-matching-questions";
diff --git a/core/application/react-query-adapter/reco/client/use-get-tailored-discoveries.ts b/core/application/react-query-adapter/reco/client/use-get-tailored-discoveries.ts
new file mode 100644
index 000000000..637ecdb12
--- /dev/null
+++ b/core/application/react-query-adapter/reco/client/use-get-tailored-discoveries.ts
@@ -0,0 +1,20 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { bootstrap } from "@/core/bootstrap";
+import { RecoFacadePort } from "@/core/domain/reco/input/reco-facade-port";
+import { GetTailoredDiscoveriesModel } from "@/core/domain/reco/reco-contract.types";
+
+import { UseQueryFacadeParams, useQueryAdapter } from "../../helpers/use-query-adapter";
+
+export function useGetTailoredDiscoveries({
+ options,
+}: UseQueryFacadeParams) {
+ const recoStoragePort = bootstrap.getRecoStoragePortForClient();
+
+ return useQuery(
+ useQueryAdapter({
+ ...recoStoragePort.getTailoredDiscoveries({}),
+ options,
+ })
+ );
+}
diff --git a/core/domain/reco/input/reco-facade-port.ts b/core/domain/reco/input/reco-facade-port.ts
index 8f2fc302e..5294bb587 100644
--- a/core/domain/reco/input/reco-facade-port.ts
+++ b/core/domain/reco/input/reco-facade-port.ts
@@ -3,6 +3,8 @@ import {
GetMatchingQuestionsPortResponse,
GetRecommendedProjectsPortParams,
GetRecommendedProjectsPortResponse,
+ GetTailoredDiscoveriesPortParams,
+ GetTailoredDiscoveriesPortResponse,
SaveMatchingQuestionsPortParams,
SaveMatchingQuestionsPortResponse,
} from "../reco-contract.types";
@@ -11,4 +13,5 @@ export interface RecoFacadePort {
getMatchingQuestions(p: GetMatchingQuestionsPortParams): GetMatchingQuestionsPortResponse;
saveMatchingQuestions(p: SaveMatchingQuestionsPortParams): SaveMatchingQuestionsPortResponse;
getRecommendedProjects(p: GetRecommendedProjectsPortParams): GetRecommendedProjectsPortResponse;
+ getTailoredDiscoveries(p: GetTailoredDiscoveriesPortParams): GetTailoredDiscoveriesPortResponse;
}
diff --git a/core/domain/reco/models/tailored-discoveries-model.ts b/core/domain/reco/models/tailored-discoveries-model.ts
new file mode 100644
index 000000000..b65f2d1ac
--- /dev/null
+++ b/core/domain/reco/models/tailored-discoveries-model.ts
@@ -0,0 +1,61 @@
+import { Issue, IssueInterface } from "@/core/domain/issue/models/issue-model";
+import { ProjectListItemInterfaceV2, ProjectListItemV2 } from "@/core/domain/project/models/project-list-item-model-v2";
+import { components } from "@/core/infrastructure/marketplace-api-client-adapter/__generated/api";
+
+export type TailoredDiscoveriesResponse = components["schemas"]["TailoredDiscoveriesResponse"];
+
+export interface TailoredDiscoveriesInterface extends TailoredDiscoveriesResponse {
+ sections: TailoredDiscoveriesSectionInterface[];
+}
+
+export class TailoredDiscoveries implements TailoredDiscoveriesInterface {
+ hasSufficientData!: TailoredDiscoveriesResponse["hasSufficientData"];
+ sections!: TailoredDiscoveriesSectionInterface[];
+
+ constructor(props: TailoredDiscoveriesResponse) {
+ Object.assign(this, props);
+ this.sections = this.sections.map(section => new TailoredDiscoveriesSection(section));
+ }
+}
+
+export type TailoredDiscoveriesSectionResponse = components["schemas"]["TailoredDiscoveriesSectionResponse"];
+
+export interface TailoredDiscoveriesSectionInterface extends TailoredDiscoveriesSectionResponse {
+ projects: ProjectListItemInterfaceV2[];
+ issues: IssueInterface[];
+ getResourceType(): "project" | "issue";
+ getProjects(): ProjectListItemInterfaceV2[];
+ getIssues(): IssueInterface[];
+}
+
+class TailoredDiscoveriesSection implements TailoredDiscoveriesSectionInterface {
+ title!: string;
+ subtitle!: string;
+ projects!: ProjectListItemInterfaceV2[];
+ issues!: IssueInterface[];
+
+ constructor(props: TailoredDiscoveriesSectionResponse) {
+ Object.assign(this, props);
+ this.projects = props.projects.map(project => new ProjectListItemV2(project));
+ this.issues = props.issues.map(issue => new Issue(issue));
+ }
+
+ private resourceTypeProject = "project" as const;
+ private resourceTypeIssue = "issue" as const;
+
+ getResourceType() {
+ if (this.projects.length > 0) {
+ return this.resourceTypeProject;
+ }
+
+ return this.resourceTypeIssue;
+ }
+
+ getProjects() {
+ return this.getResourceType() === this.resourceTypeProject ? this.projects : [];
+ }
+
+ getIssues() {
+ return this.getResourceType() === this.resourceTypeIssue ? this.issues : [];
+ }
+}
diff --git a/core/domain/reco/output/reco-storage-port.ts b/core/domain/reco/output/reco-storage-port.ts
index 3844998fa..9457ead24 100644
--- a/core/domain/reco/output/reco-storage-port.ts
+++ b/core/domain/reco/output/reco-storage-port.ts
@@ -3,6 +3,8 @@ import {
GetMatchingQuestionsPortResponse,
GetRecommendedProjectsPortParams,
GetRecommendedProjectsPortResponse,
+ GetTailoredDiscoveriesPortParams,
+ GetTailoredDiscoveriesPortResponse,
SaveMatchingQuestionsPortParams,
SaveMatchingQuestionsPortResponse,
} from "../reco-contract.types";
@@ -11,4 +13,5 @@ export interface RecoStoragePort {
getMatchingQuestions(p: GetMatchingQuestionsPortParams): GetMatchingQuestionsPortResponse;
saveMatchingQuestions(p: SaveMatchingQuestionsPortParams): SaveMatchingQuestionsPortResponse;
getRecommendedProjects(p: GetRecommendedProjectsPortParams): GetRecommendedProjectsPortResponse;
+ getTailoredDiscoveries(p: GetTailoredDiscoveriesPortParams): GetTailoredDiscoveriesPortResponse;
}
diff --git a/core/domain/reco/reco-contract.types.ts b/core/domain/reco/reco-contract.types.ts
index e8c00e335..9d22dc8b9 100644
--- a/core/domain/reco/reco-contract.types.ts
+++ b/core/domain/reco/reco-contract.types.ts
@@ -1,3 +1,4 @@
+import { TailoredDiscoveriesInterface } from "@/core/domain/reco/models/tailored-discoveries-model";
import { components, operations } from "@/core/infrastructure/marketplace-api-client-adapter/__generated/api";
import {
HttpClientParameters,
@@ -50,3 +51,13 @@ export type GetRecommendedProjectsPortResponse = HttpStorageResponse;
+
+/* ------------------------------ Get Tailored Discoveries ------------------------------ */
+
+export type GetTailoredDiscoveriesResponse = components["schemas"]["TailoredDiscoveriesResponse"];
+
+export type GetTailoredDiscoveriesModel = TailoredDiscoveriesInterface;
+
+export type GetTailoredDiscoveriesPortResponse = HttpStorageResponse;
+
+export type GetTailoredDiscoveriesPortParams = HttpClientParameters