Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] discover v2 #1045

Merged
merged 7 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions app/(saas)/discover/_components/issue-card/issue-card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1">
{languages.slice(0, 2).map(language => (
<Avatar className="size-5" key={language.name}>
<AvatarImage src={language.logoUrl} />
<AvatarFallback className="rounded-xl">{language.name.charAt(0)}</AvatarFallback>
</Avatar>
))}
</div>
</TooltipTrigger>

<TooltipContent side="bottom" align="end">
<ul className="flex flex-col gap-2">
{languages.map(language => (
<li key={language.name} className="flex items-center gap-1">
<Avatar className="size-5" key={language.name}>
<AvatarImage src={language.logoUrl} />
<AvatarFallback className="rounded-xl">{language.name.charAt(0)}</AvatarFallback>
</Avatar>
<TypographySmall>{language.name}</TypographySmall>
</li>
))}
</ul>
</TooltipContent>
</Tooltip>
);
}, [languages]);

return (
<Card className="flex flex-col gap-4 p-4">
<header>
<CardTitle className="flex w-full flex-row items-center justify-start gap-2">
<ContributionBadge type="ISSUE" githubStatus={issue.githubStatus} number={issue.number} />
<div className="line-clamp-1 flex-1">{title}</div>
</CardTitle>
</header>

<div className="flex flex-row items-center gap-1">
<Avatar className="size-5">
<AvatarImage src={project.logoUrl} />
<AvatarFallback>{project.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex w-full flex-row items-center justify-between gap-4">
<TypographyMuted>
{project.name}/{project.repo}
</TypographyMuted>

{renderLanguages()}
</div>
</div>

<footer className="flex flex-row items-center justify-between gap-1">
<div className="flex flex-row items-center justify-end gap-1">
{limitedLabels.map(label => (
<Badge variant={"secondary"} key={label}>
{label}
</Badge>
))}
</div>
<TypographyMuted>{dateKernelPort.formatDistanceToNow(new Date(createdAt))}</TypographyMuted>
</footer>
</Card>
);
}
20 changes: 20 additions & 0 deletions app/(saas)/discover/_components/issue-card/issue-card.types.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
117 changes: 117 additions & 0 deletions app/(saas)/discover/_components/new-project-card/new-project-card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-md">
<div className="flex items-center gap-sm">
<Star className="size-3.5 shrink-0 text-muted-foreground" />

<TypographySmall className="text-muted-foreground">{Intl.NumberFormat().format(stars)}</TypographySmall>
</div>
<div className="flex items-center gap-sm">
<GitFork className="size-3.5 shrink-0 text-muted-foreground" />

<TypographySmall className="text-muted-foreground">{Intl.NumberFormat().format(forks)}</TypographySmall>
</div>
<div className="flex items-center gap-sm">
<UserRound className="size-3.5 shrink-0 text-muted-foreground" />

<TypographySmall className="text-muted-foreground">{Intl.NumberFormat().format(contributors)}</TypographySmall>
</div>
</div>
);
}

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 (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1">
{languages.slice(0, 2).map(language => (
<Avatar className="size-5" key={language.name}>
<AvatarImage src={language.logoUrl} />
<AvatarFallback className="rounded-xl">{language.name.charAt(0)}</AvatarFallback>
</Avatar>
))}
</div>
</TooltipTrigger>

<TooltipContent side="bottom" align="end">
<ul className="flex flex-col gap-2">
{languages?.map(language => (
<li key={language.name} className="flex items-center justify-between gap-10">
<div className="flex items-center gap-1">
<Avatar className="size-5" key={language.name}>
<AvatarImage src={language.logoUrl} />
<AvatarFallback className="rounded-xl">{language.name.charAt(0)}</AvatarFallback>
</Avatar>
<TypographySmall>{language.name}</TypographySmall>
</div>

<TypographySmall>{language.percentage}%</TypographySmall>
</li>
))}
</ul>
</TooltipContent>
</Tooltip>
);
}, [languages]);

return (
<Card className={cn("flex flex-col gap-4 p-4", className)}>
<CardTitle className="flex w-full flex-row items-center justify-start gap-2">
<Avatar className="size-12 rounded-xl">
<AvatarImage src={logoUrl} />
<AvatarFallback className="rounded-xl">{name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<div className="line-clamp-1 flex-1">
<TypographyH4>{name}</TypographyH4>
</div>
<Metrics stars={stars} forks={forks} contributors={contributors} />
</div>
</CardTitle>

<CardContent className="flex-1 p-0">
<TypographyMuted className="line-clamp-3">{description}</TypographyMuted>
</CardContent>

<CardFooter className="flex flex-row items-center justify-between gap-4 p-0">
<div className="flex flex-row items-center justify-end gap-1">
{limitedCategories.map(label => (
<Badge variant={"secondary"} key={label}>
{label}
</Badge>
))}
</div>

{renderLanguages()}
</CardFooter>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -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;
}
58 changes: 58 additions & 0 deletions app/(saas)/discover/_components/page-banner/page-banner.tsx
Original file line number Diff line number Diff line change
@@ -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 <Skeleton className="h-[254px] w-full" />;
}

if (isError) return <ErrorState />;

if (!data)
return (
<ListBanner
title={{
children: (
<>
Embark on an <span className="text-indigo-500">ODQuest</span> Adventure
</>
),
}}
subtitle={{
children:
"Unlock epic rewards by conquering challenges and join a thriving community of adventurers on an exciting Quest!",
}}
logo={<Target className="size-16 text-indigo-500" />}
classNames={{
base: "bg-gradient-to-br from-indigo-900 to-transparent to-80%",
}}
>
<Button size="sm" asChild>
<Link href={NEXT_ROUTER.quests.root}>Join now</Link>
</Button>
</ListBanner>
);

const liveHackathon = data.hackathons.find(hackathon => hackathon.isLive());

if (!liveHackathon) return null;

return <LiveHackathonCard hackathon={liveHackathon} />;
}, [data, isError, isLoading]);

return renderLiveHackathon();
}
62 changes: 62 additions & 0 deletions app/(saas)/discover/_components/page-carousel/page-carousel.tsx
Original file line number Diff line number Diff line change
@@ -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" ? (
<CircleDotDashed className="size-4 text-green-500" />
) : (
<Folder className="size-4 text-cyan-500" />
);

return (
<Badge variant={"secondary"} className="flex flex-row items-center gap-2">
{badgeIcon}
{badgeLabel}
</Badge>
);
}

export function PageCarousel({ children, title, description, count, resourceType }: PageCarouselProps) {
const childrenArray = Children.toArray(children) as ReactElement[];

return (
<section className="w-full">
<Carousel plugins={[WheelGesturesPlugin()]}>
<div className="flex w-full flex-row items-start justify-between">
<div className="flex flex-col gap-2">
<div className="flex flex-row items-center justify-start gap-2">
<ResourceBadge resourceType={resourceType} />
<TypographyH3>
{title}
{count && ` (${count})`}
</TypographyH3>
</div>
<TypographyMuted>{description}</TypographyMuted>
</div>
<div className="flex flex-row items-center justify-end gap-2">
<CarouselPrevious className="static translate-y-0" />
<CarouselNext className="static translate-y-0" />
</div>
</div>
<div className="mt-3 w-full">
<CarouselContent className="-ml-6">
{childrenArray.map((child, index) => (
<CarouselItem key={index} className="pl-6 md:basis-1/2 lg:basis-1/3">
{child}
</CarouselItem>
))}
</CarouselContent>
</div>
</Carousel>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactNode } from "react";

export interface PageCarouselProps {
resourceType: "issue" | "project";
title: string;
description?: string;
count?: number;
children: ReactNode[];
}
Loading
Loading