diff --git a/CHANGELOG.md b/CHANGELOG.md index abd798e560..b0b6d62622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,53 @@ > All notable changes to this project will be documented in this file +## [2.44.0-beta.6](https://github.com/open-sauced/app/compare/v2.44.0-beta.5...v2.44.0-beta.6) (2024-07-11) + + +### πŸ• Features + +* filter `RossChart` by contributor type ([#3698](https://github.com/open-sauced/app/issues/3698)) ([d48e8ac](https://github.com/open-sauced/app/commit/d48e8ac55e34136540277c1f7bb9dac2881d984c)) + + +### πŸ› Bug Fixes + +* allow repos and contributors to be added to workspaces from explore page ([#3700](https://github.com/open-sauced/app/issues/3700)) ([3271dd4](https://github.com/open-sauced/app/commit/3271dd4f086df90b79a8ad5a704ea49a1f528602)) + +## [2.44.0-beta.5](https://github.com/open-sauced/app/compare/v2.44.0-beta.4...v2.44.0-beta.5) (2024-07-11) + + +### πŸ• Features + +* add Contributor Distribution to repo page ([#3712](https://github.com/open-sauced/app/issues/3712)) ([a53c7cd](https://github.com/open-sauced/app/commit/a53c7cd57652c75ecc52a9650a8aed9dbc9ac9c7)) + +## [2.44.0-beta.4](https://github.com/open-sauced/app/compare/v2.44.0-beta.3...v2.44.0-beta.4) (2024-07-11) + + +### πŸ• Features + +* allow input and display of GitHub releases as highlights ([#3705](https://github.com/open-sauced/app/issues/3705)) ([8aa0177](https://github.com/open-sauced/app/commit/8aa01775eb4f93491459bbcd571293bf207e66e4)) + +## [2.44.0-beta.3](https://github.com/open-sauced/app/compare/v2.44.0-beta.2...v2.44.0-beta.3) (2024-07-10) + + +### πŸ› Bug Fixes + +* now a contributor insight can be deleted if one was deleted in the same page session ([#3246](https://github.com/open-sauced/app/issues/3246)) ([8dd1ee2](https://github.com/open-sauced/app/commit/8dd1ee25d4bfbee7d78191609e205e0cb62f7213)) + +## [2.44.0-beta.2](https://github.com/open-sauced/app/compare/v2.44.0-beta.1...v2.44.0-beta.2) (2024-07-10) + + +### πŸ› Bug Fixes + +* now the contributions tab is the first tab on the user profile page ([#3709](https://github.com/open-sauced/app/issues/3709)) ([87d0ee9](https://github.com/open-sauced/app/commit/87d0ee9f72a002de2cffb0df1d335add01303330)) + +## [2.44.0-beta.1](https://github.com/open-sauced/app/compare/v2.43.0...v2.44.0-beta.1) (2024-07-10) + + +### πŸ• Features + +* implemented the Radar chart component ([#3704](https://github.com/open-sauced/app/issues/3704)) ([8579f41](https://github.com/open-sauced/app/commit/8579f416f3077108631d04d88833a47d43896e17)) + ## [2.43.0](https://github.com/open-sauced/app/compare/v2.42.0...v2.43.0) (2024-07-09) diff --git a/components/Graphs/RadarChart.stories.tsx b/components/Graphs/RadarChart.stories.tsx new file mode 100644 index 0000000000..9f148516e8 --- /dev/null +++ b/components/Graphs/RadarChart.stories.tsx @@ -0,0 +1,45 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ChartTooltipContent } from "components/primitives/chart-primitives"; +import { RadarChart } from "./RadarChart"; + +type Story = StoryObj; + +const meta: Meta = { + title: "Components/Graphs/RadarChart", + component: RadarChart, + args: { + radarDataKey: "percentage", + polarAngleAxisDataKey: "type", + data: [ + { type: "Code review", percentage: 30 }, + { type: "Issues", percentage: 11 }, + { type: "Pull requests", percentage: 11 }, + { type: "Commits", percentage: 48 }, + ], + fill: "#ff5100", + }, +}; + +export default meta; + +export const Default: Story = {}; +export const WithDot: Story = { + args: { + dot: { + r: 4, + fillOpacity: 1, + }, + }, +}; +export const WithCustomTooltip: Story = { + args: { + chartTooltipContent: ( + { + return `${value}%`; + }} + /> + ), + }, +}; diff --git a/components/Graphs/RadarChart.tsx b/components/Graphs/RadarChart.tsx new file mode 100644 index 0000000000..cb475c3983 --- /dev/null +++ b/components/Graphs/RadarChart.tsx @@ -0,0 +1,57 @@ +"use client"; + +// If you want to make improvements to the chart or extend it, see the examples at https://ui.shadcn.com/charts#radar-chart + +import { PolarAngleAxis, PolarGrid, Radar, RadarChart as RawRadarChart } from "recharts"; +import { ComponentProps } from "react"; +import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "components/primitives/chart-primitives"; + +const chartConfig = { + desktop: { + label: "Desktop", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + +interface RadarChartProps { + data: ComponentProps["data"]; + cursor?: boolean; + radarDataKey: ComponentProps["dataKey"]; + polarAngleAxisDataKey: ComponentProps["dataKey"]; + children?: React.ReactNode; + opacity?: number; + // create an optional prop for the type that is the type of the RawRadarChart dot prop, but infer it from it's existing type + dot?: ComponentProps["dot"]; + fill: ComponentProps["fill"]; + // If you need a diffent unit, you can add it here + maxHeight?: `${number}${"px" | "rem"}` | "auto"; + labelFormatter?: ComponentProps["labelFormatter"]; + formatter?: ComponentProps["formatter"]; + chartTooltipContent?: ComponentProps["content"]; +} + +export function RadarChart({ + data, + radarDataKey, + polarAngleAxisDataKey, + dot, + fill, + cursor = false, + opacity = 0.6, + maxHeight = "250px", + chartTooltipContent, +}: RadarChartProps) { + return ( + + + } + /> + + + + + + ); +} diff --git a/components/Repositories/RossChart.tsx b/components/Repositories/RossChart.tsx index a2f8fb67f3..8f48f38b44 100644 --- a/components/Repositories/RossChart.tsx +++ b/components/Repositories/RossChart.tsx @@ -1,7 +1,7 @@ import { FaUsers } from "react-icons/fa6"; import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts"; import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import Card from "components/atoms/Card/card"; import SkeletonWrapper from "components/atoms/SkeletonLoader/skeleton-wrapper"; import humanizeNumber from "lib/utils/humanizeNumber"; @@ -11,34 +11,82 @@ type RossChartProps = { isLoading: boolean; error: Error | undefined; range: number; - rangedTotal: number; className?: string; }; -export default function RossChart({ stats, rangedTotal, isLoading, error, range, className }: RossChartProps) { - const rangedAverage = useMemo(() => (rangedTotal / range).toPrecision(2), [rangedTotal, range]); +export default function RossChart({ stats, isLoading, error, range, className }: RossChartProps) { + const [filterOutside, setFilterOutside] = useState(true); + const [filterRecurring, setFilterRecurring] = useState(true); + const [filterInternal, setFilterInternal] = useState(true); + + const filteredTotal = useMemo(() => { + return ( + stats?.contributors.reduce((prev, curr) => { + return (prev += + (filterOutside ? curr.new : 0) + + (filterRecurring ? curr.recurring : 0) + + (filterInternal ? curr.internal : 0)); + }, 0) || 0 + ); + }, [stats, filterOutside, filterRecurring, filterInternal]); + + const rangedAverage = useMemo( + () => (filteredTotal / (stats ? stats.contributors.length : 1)).toPrecision(2), + [filteredTotal, stats] + ); const weeklyData = useMemo(() => { - const result = stats?.contributors.reverse().map((week) => { + return stats?.contributors.reverse().map((week) => { return { - ...week, + new: filterOutside ? week.new : 0, + recurring: filterRecurring ? week.recurring : 0, + internal: filterInternal ? week.internal : 0, bucket: new Date(week.bucket).toLocaleDateString(undefined, { month: "numeric", day: "numeric" }), }; }); - - return result; - }, [stats]); + }, [stats, filterOutside, filterRecurring, filterInternal]); const bucketTicks = useMemo(() => { - const result = stats?.contributors.reverse().map((week) => { + return stats?.contributors.reverse().map((week) => { return new Date(week.bucket).toLocaleDateString(undefined, { month: "numeric", day: "numeric" }); }); - - return result; }, [stats]); + const CONTRIBUTOR_COLORS: Record = { + internal: "#1E3A8A", + recurring: "#2563EB", + new: "#60A5FA", + }; + + function CustomTooltip({ active, payload }: TooltipProps) { + if (active && payload) { + const legend = payload.reverse(); + return ( +
+
+ +

Contributors

+

{payload[0]?.payload.bucket}

+
+ + {legend.map((data) => ( +
+

+ + {data.name === "new" ? "Outside" : data.name}: +

+

{data.value}

+
+ ))} +
+ ); + } + } + return ( - +
{isLoading ? ( @@ -54,10 +102,10 @@ export default function RossChart({ stats, rangedTotal, isLoading, error, range, @@ -79,57 +127,48 @@ export default function RossChart({ stats, rangedTotal, isLoading, error, range, /> - - - + {filterInternal && } + {filterRecurring && } + {filterOutside && } )} + +
+ + + + + +
); } -function CustomTooltip({ active, payload }: TooltipProps) { - if (active && payload) { - return ( -
-
- -

Contributors

-

{payload[0]?.payload.bucket}

-
- {payload[2]?.value && ( -
-

- - New: -

-

{payload[2]?.value}

-
- )} - {payload[1]?.value && ( -
-

- - Recurring: -

-

{payload[1]?.value}

-
- )} - {payload[0]?.value && ( -
-

- - Internal: -

-

{payload[0]?.value}

-
- )} -
- ); - } -} - function CustomTick({ x, y, payload }: { x: number; y: number; payload: { value: string } }) { return ( diff --git a/components/atoms/TextInput/text-input.tsx b/components/atoms/TextInput/text-input.tsx index a7f0b096f8..2e5ad241af 100644 --- a/components/atoms/TextInput/text-input.tsx +++ b/components/atoms/TextInput/text-input.tsx @@ -32,8 +32,10 @@ const TextInput = ({ const handleResetInput = () => { handleChange?.(""); - if (fieldRef) { + if (fieldRef?.current) { fieldRef.current!.value = ""; + } else if (inputRef.current) { + inputRef.current.value = ""; } }; diff --git a/components/molecules/ContributorHighlight/contributor-highlight-card.tsx b/components/molecules/ContributorHighlight/contributor-highlight-card.tsx index f31e908ccf..aeec683e27 100644 --- a/components/molecules/ContributorHighlight/contributor-highlight-card.tsx +++ b/components/molecules/ContributorHighlight/contributor-highlight-card.tsx @@ -3,6 +3,7 @@ import { HiOutlineEmojiHappy } from "react-icons/hi"; import { TfiMoreAlt } from "react-icons/tfi"; import { FiEdit, FiLinkedin } from "react-icons/fi"; import { BsCalendar2Event, BsLink45Deg, BsTagFill } from "react-icons/bs"; +import { GoTag } from "react-icons/go"; import { FaXTwitter } from "react-icons/fa6"; import { GrFlag } from "react-icons/gr"; import Emoji from "react-emoji-render"; @@ -85,7 +86,7 @@ interface ContributorHighlightCardProps { type?: HighlightType; taggedRepos: string[]; } -export type HighlightType = "pull_request" | "issue" | "blog_post"; +export type HighlightType = "pull_request" | "issue" | "blog_post" | "release"; const ContributorHighlightCard = ({ title, @@ -278,6 +279,8 @@ const ContributorHighlightCard = ({ const getHighlightTypePreset = (type: HighlightType): { text: string; icon?: React.ReactElement } => { switch (type) { + case "release": + return { text: "Release", icon: }; case "pull_request": return { text: "Pull request", icon: }; case "blog_post": diff --git a/components/molecules/HighlightInput/highlight-input-form.tsx b/components/molecules/HighlightInput/highlight-input-form.tsx index 561061e2ca..b8698cd696 100644 --- a/components/molecules/HighlightInput/highlight-input-form.tsx +++ b/components/molecules/HighlightInput/highlight-input-form.tsx @@ -28,6 +28,7 @@ import { isValidPullRequestUrl, getAvatarByUsername, generateRepoParts, + isValidReleaseUrl, } from "lib/utils/github"; import { fetchGithubPRInfo } from "lib/hooks/fetchGithubPRInfo"; @@ -456,7 +457,9 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E setIsHighlightURLValid(true); // generateApiPrUrl will return an object with repoName, orgName and issueId // it can work with both issue and pull request links - const highlightType = isValidIssueUrl(highlightLink) + const highlightType = isValidReleaseUrl(highlightLink) + ? "release" + : isValidIssueUrl(highlightLink) ? "issue" : isValidPullRequestUrl(highlightLink) ? "pull_request" diff --git a/components/organisms/ContributorProfileTab/contributor-profile-tab.tsx b/components/organisms/ContributorProfileTab/contributor-profile-tab.tsx index 36bdb25061..9bb113ab71 100644 --- a/components/organisms/ContributorProfileTab/contributor-profile-tab.tsx +++ b/components/organisms/ContributorProfileTab/contributor-profile-tab.tsx @@ -62,8 +62,8 @@ interface QueryParams { } const tabs: Record = { - highlights: "Highlights", contributions: "Contributions", + highlights: "Highlights", recommendations: "Recommendations", }; diff --git a/components/organisms/Contributors/contributors.tsx b/components/organisms/Contributors/contributors.tsx index 4adbed85fe..ba7108dfc9 100644 --- a/components/organisms/Contributors/contributors.tsx +++ b/components/organisms/Contributors/contributors.tsx @@ -22,6 +22,7 @@ import { useMediaQuery } from "lib/hooks/useMediaQuery"; import ClientOnly from "components/atoms/ClientOnly/client-only"; import { setQueryParams } from "lib/utils/query-params"; +import useSupabaseAuth from "lib/hooks/useSupabaseAuth"; import ContributorCard from "../ContributorCard/contributor-card"; import ContributorTable from "../ContributorsTable/contributors-table"; @@ -29,9 +30,15 @@ interface ContributorProps { repositories?: number[]; title?: string; defaultLayout?: ToggleValue; + personalWorkspaceId?: string; } -const Contributors = ({ repositories, title, defaultLayout = "list" }: ContributorProps): JSX.Element => { +const Contributors = ({ + repositories, + title, + defaultLayout = "list", + personalWorkspaceId, +}: ContributorProps): JSX.Element => { const router = useRouter(); const limit = router.query.limit as string; const topic = router.query.pageId as string; @@ -39,6 +46,7 @@ const Contributors = ({ repositories, title, defaultLayout = "list" }: Contribut const { data, meta, setPage, isError, isLoading } = useContributors(Number(limit ?? 10), repositories); const { toast } = useToast(); + const { user, signIn } = useSupabaseAuth(); const [layout, setLayout] = useState(defaultLayout); const [selectedContributors, setSelectedContributors] = useState([]); const [selectedListIds, setSelectedListIds] = useState([]); @@ -80,21 +88,22 @@ const Contributors = ({ repositories, title, defaultLayout = "list" }: Contribut selectedListIds.map((listIds) => addListContributor( listIds, - selectedContributors.map((contributor) => ({ id: contributor.user_id })) + selectedContributors.map((contributor) => ({ id: contributor.user_id })), + workspaceId || personalWorkspaceId ) ) ); response .then((res) => { toast({ - description: `Successfully added ${selectedContributors.length} contributors to ${selectedListIds.length} lists!`, + description: `Successfully added ${selectedContributors.length} contributors to ${selectedListIds.length} insights!`, variant: "success", }); }) .catch((res) => { toast({ description: ` - An error occurred while adding contributors to lists. Please try again. + An error occurred while adding contributors to an insight. Please try again. `, variant: "danger", }); @@ -139,8 +148,9 @@ const Contributors = ({ repositories, title, defaultLayout = "list" }: Contribut
@@ -198,11 +208,20 @@ const Contributors = ({ repositories, title, defaultLayout = "list" }: Contribut { - setPopoverOpen(value); + if (!user) { + signIn({ + provider: "github", + options: { + redirectTo: `${window.location.href}`, + }, + }); + } else { + setPopoverOpen(value); + } }} > - + {popoverOpen && } diff --git a/components/organisms/ListPage/DeleteListPageModal.tsx b/components/organisms/ListPage/DeleteListPageModal.tsx index 61f743e445..5325279ae6 100644 --- a/components/organisms/ListPage/DeleteListPageModal.tsx +++ b/components/organisms/ListPage/DeleteListPageModal.tsx @@ -15,15 +15,7 @@ interface ModalProps { isLoading?: boolean; } -const DeleteListPageModal: FC = ({ - open = false, - setOpen, - submitted = false, - listName, - onConfirm, - onClose, - isLoading, -}) => { +const DeleteListPageModal: FC = ({ open = false, setOpen, listName, onConfirm, onClose, isLoading }) => { const [input, setInput] = useState(""); const handleOnNameChange = (e: React.ChangeEvent) => { @@ -40,7 +32,7 @@ const DeleteListPageModal: FC = ({ setInput(""); }; - const disabled = input !== listName || submitted; + const disabled = input !== listName; return ( diff --git a/components/organisms/Repositories/repositories.tsx b/components/organisms/Repositories/repositories.tsx index 8a5f4184ef..938177c5c4 100644 --- a/components/organisms/Repositories/repositories.tsx +++ b/components/organisms/Repositories/repositories.tsx @@ -52,7 +52,12 @@ export default function Repositories({ repositories, showSearch = true }: Reposi }; const handleOnAddtoInsights = () => { - if (user) { + if (!workspaceId) { + router.push({ + pathname: `/workspaces/new`, + query: { repos: JSON.stringify(selectedRepos.map((repo) => repo.full_name)) }, + }); + } else if (user) { router.push({ pathname: `/workspaces/${workspaceId}/repository-insights/new`, query: { repos: JSON.stringify(selectedRepos.map((repo) => repo.full_name)) }, @@ -143,10 +148,16 @@ export default function Repositories({ repositories, showSearch = true }: Reposi {selectedRepos.length > 0 && ( -
+
0 ? "flex" : "hidden", + `justify-between p-3 px-6 items-center border-b-2 text-light-slate-11` + )} + >
{selectedRepos.length} Repositories selected
)} diff --git a/components/organisms/ToolsDisplay/tools-display.tsx b/components/organisms/ToolsDisplay/tools-display.tsx index e1c329d72f..709c8bc00a 100644 --- a/components/organisms/ToolsDisplay/tools-display.tsx +++ b/components/organisms/ToolsDisplay/tools-display.tsx @@ -27,7 +27,7 @@ const Tool = ({ tool, repositories }: ToolProps): JSX.Element => { case "Dashboard": return ; case "Contributors": - return ; + return ; case "Activity": return ; diff --git a/components/primitives/chart-primitives.tsx b/components/primitives/chart-primitives.tsx new file mode 100644 index 0000000000..cae73d11f7 --- /dev/null +++ b/components/primitives/chart-primitives.tsx @@ -0,0 +1,303 @@ +"use client"; + +import clsx from "clsx"; +import * as RechartsPrimitive from "recharts"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ({ color?: string; theme?: never } | { color?: never; theme: Record }); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + {children} +
+
+ ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color); + + if (!colorConfig.length) { + return null; + } + + return ( +