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]); + + return ( + +
+ + +
{title}
+
+
+ +
+ + + {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]); + + return ( + + + + + {name.charAt(0)} + +
+
+ {name} +
+ +
+
+ + + {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; diff --git a/core/infrastructure/marketplace-api-client-adapter/adapters/reco-client-adapter.ts b/core/infrastructure/marketplace-api-client-adapter/adapters/reco-client-adapter.ts index c4f58e43a..6d4492450 100644 --- a/core/infrastructure/marketplace-api-client-adapter/adapters/reco-client-adapter.ts +++ b/core/infrastructure/marketplace-api-client-adapter/adapters/reco-client-adapter.ts @@ -1,9 +1,11 @@ import { ProjectListItemV2 } from "@/core/domain/project/models/project-list-item-model-v2"; import { MatchingQuestions } from "@/core/domain/reco/models/matching-questions-model"; +import { TailoredDiscoveries } from "@/core/domain/reco/models/tailored-discoveries-model"; import { RecoStoragePort } from "@/core/domain/reco/output/reco-storage-port"; import { GetMatchingQuestionsResponse, GetRecommendedProjectsResponse, + GetTailoredDiscoveriesResponse, SaveMatchingQuestionsBody, } from "@/core/domain/reco/reco-contract.types"; import { FirstParameter } from "@/core/kernel/types"; @@ -17,6 +19,7 @@ export class RecoClientAdapter implements RecoStoragePort { getMatchingQuestions: "me/reco/projects/matching-questions", saveMatchingQuestions: "me/reco/projects/matching-questions/:questionId/answers", getRecommendedProjects: "me/reco/projects", + getTailoredDiscoveries: "me/reco/discoveries", } as const; getMatchingQuestions = ({ queryParams }: FirstParameter) => { @@ -88,4 +91,25 @@ export class RecoClientAdapter implements RecoStoragePort { tag, }; }; + + getTailoredDiscoveries = () => { + const path = this.routes["getTailoredDiscoveries"]; + const method = "GET"; + const tag = HttpClient.buildTag({ path }); + + const request = async () => { + const data = await this.client.request({ + path, + method, + tag, + }); + + return new TailoredDiscoveries(data); + }; + + return { + request, + tag, + }; + }; } diff --git a/core/infrastructure/marketplace-api-client-adapter/mock-adapters/reco-client-adapter-mock.ts b/core/infrastructure/marketplace-api-client-adapter/mock-adapters/reco-client-adapter-mock.ts index 5713c7036..54b6df27e 100644 --- a/core/infrastructure/marketplace-api-client-adapter/mock-adapters/reco-client-adapter-mock.ts +++ b/core/infrastructure/marketplace-api-client-adapter/mock-adapters/reco-client-adapter-mock.ts @@ -7,6 +7,10 @@ export class RecoClientAdapterMock implements RecoStoragePort { routes = {}; getMatchingQuestions = mockHttpStorageResponse; + saveMatchingQuestions = mockHttpStorageResponse; + getRecommendedProjects = mockHttpStorageResponse; + + getTailoredDiscoveries = mockHttpStorageResponse; } diff --git a/public/images/backgrounds/discover-header.png b/public/images/backgrounds/discover-header.png new file mode 100644 index 000000000..e0726e5c8 Binary files /dev/null and b/public/images/backgrounds/discover-header.png differ diff --git a/shared/features/list-banner/list-banner.tsx b/shared/features/list-banner/list-banner.tsx index b9fc4d3d0..3d739a6c9 100644 --- a/shared/features/list-banner/list-banner.tsx +++ b/shared/features/list-banner/list-banner.tsx @@ -2,19 +2,29 @@ import { Paper } from "@/design-system/atoms/paper/variants/paper-default"; import { Typo } from "@/design-system/atoms/typo/variants/typo-default"; import { ListBannerProps } from "@/shared/features/list-banner/list-banner.types"; +import { cn } from "@/shared/utils"; + +export function ListBanner({ title, subtitle, logo, classNames, children }: ListBannerProps) { + const { base, ...restClassNames } = classNames ?? {}; -export function ListBanner({ title, subtitle, logo }: ListBannerProps) { return ( -
- - +
+
+ + +
+ + {children ?
{children}
: null}
; subtitle: TypoPort<"p">; logo?: ReactNode; -}; + classNames?: { + base?: string; + }; +} diff --git a/shared/features/page/page-container/page-container.tsx b/shared/features/page/page-container/page-container.tsx index e10605a4b..f178acf3a 100644 --- a/shared/features/page/page-container/page-container.tsx +++ b/shared/features/page/page-container/page-container.tsx @@ -1,22 +1,18 @@ import { PropsWithChildren } from "react"; import { SidePanelsProvider } from "@/shared/features/side-panels/side-panels.context"; -import { cn } from "@/shared/utils"; + +import { PageInner } from "../page-inner/page-inner"; +import { PageInnerProps } from "../page-inner/page-inner.types"; export function PageContainer({ children, size = "small", className, -}: PropsWithChildren<{ size?: "small" | "medium" | "large"; className?: string }>) { +}: PropsWithChildren<{ size?: PageInnerProps["size"]; className?: string }>) { return ( -
+ {children} -
+ ); } diff --git a/shared/features/page/page-inner/page-inner.tsx b/shared/features/page/page-inner/page-inner.tsx new file mode 100644 index 000000000..8ce0b3bd2 --- /dev/null +++ b/shared/features/page/page-inner/page-inner.tsx @@ -0,0 +1,19 @@ +import { cn } from "@/shared/utils"; + +import { PageInnerProps } from "./page-inner.types"; + +export function PageInner({ children, size = "small", className, type = "inner" }: PageInnerProps) { + return ( +
+ {children} +
+ ); +} diff --git a/shared/features/page/page-inner/page-inner.types.ts b/shared/features/page/page-inner/page-inner.types.ts new file mode 100644 index 000000000..5c7c84187 --- /dev/null +++ b/shared/features/page/page-inner/page-inner.types.ts @@ -0,0 +1,9 @@ +import { PropsWithChildren } from "react"; + +export type PageInnerSize = "small" | "medium" | "large" | "full"; + +export interface PageInnerProps extends PropsWithChildren { + size?: PageInnerSize; + className?: string; + type?: "page" | "inner"; +}