Skip to content

Commit

Permalink
feat: implement 'Add to workspace' in repo pages (#3282)
Browse files Browse the repository at this point in the history
  • Loading branch information
zeucapua committed May 1, 2024
1 parent 9a4cab1 commit b1efc46
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 13 deletions.
117 changes: 117 additions & 0 deletions components/Repositories/AddToWorkspaceDrawer.tsx
@@ -0,0 +1,117 @@
import { MdWorkspaces } from "react-icons/md";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { BsGithub } from "react-icons/bs";
import Button from "components/shared/Button/button";
import { Drawer } from "components/shared/Drawer";
import { useToast } from "lib/hooks/useToast";
import useSupabaseAuth from "lib/hooks/useSupabaseAuth";
import useWorkspaces from "lib/hooks/api/useWorkspaces";
import { fetchApiData } from "helpers/fetchApiData";
import SingleSelect from "components/atoms/Select/single-select";
import Text from "components/atoms/Typography/text";

export default function AddToWorkspaceDrawer({ repository }: { repository: string }) {
const router = useRouter();
const { toast } = useToast();
const { signIn, user, sessionToken } = useSupabaseAuth();
const [workspaceId, setWorkspaceId] = useState("new");
const { data: workspaces, isLoading: workspacesLoading, mutate } = useWorkspaces({ load: !!user, limit: 100 });

const [host, setHost] = useState("");
useEffect(() => {
if (typeof window !== "undefined") {
setHost(window.location.origin as string);
}
}, []);

const addRepositoryToWorkspace = async () => {
if (workspaceId === "new") {
router.push(`/workspaces/new?repos=${JSON.stringify([repository])}`);
return;
}

const { data, error } = await fetchApiData<Workspace>({
method: "POST",
path: `workspaces/${workspaceId}/repos/${repository}`,
body: {}, // empty body needed to avoid error
bearerToken: sessionToken!,
pathValidator: () => true,
});

if (error) {
toast({ description: `Error adding repository to the workspace. Please try again`, variant: "danger" });
} else {
toast({ description: `Added repository successfully`, variant: "success" });
}
};
return (
<Drawer
title="Add repository to Workspace"
description="Create a new workspace or add to an existing one."
showCloseButton
trigger={
<Button variant="primary" className="shrink-0 items-center gap-3 w-fit">
<MdWorkspaces />
Add to Workspace
</Button>
}
>
{!user ? (
<div className="flex flex-col gap-4 text-center">
<img
src="/assets/workspace_overview.png"
alt="Workspace screenshot from documentation"
className="border-2 border-light-orange-9 shadow-md rounded-lg"
/>
<Text>
Keep track of repositories and contributors easily with our new feature
<span className="font-semibold"> Workspaces!</span> If you&apos;ve used OpenSauced before, your insights and
lists are now part of your personal workspace.
</Text>
<p className="font-medium text-light-orange-10">
Create a new workspace with this repository and explore open source like never before!
</p>
<Button
variant="primary"
className="w-fit gap-2 self-center"
onClick={() => {
signIn({
provider: "github",
options: { redirectTo: `${host}/workspaces/new?repos=${JSON.stringify([repository])}` },
});
}}
>
<BsGithub className="w-5 h-5" />
Connect with GitHub
</Button>
</div>
) : (
<>
{workspacesLoading ? (
<p>Loading...</p>
) : (
<SingleSelect
options={[
{ label: "Create new workspace...", value: "new" },
...workspaces.map(({ id, name }) => ({
label: name,
value: id,
})),
]}
position="popper"
value={workspaceId ?? "new"}
placeholder="Select a workspace"
onValueChange={(value) => {
setWorkspaceId(value);
}}
/>
)}
<Button onClick={addRepositoryToWorkspace} variant="primary" className="w-full !grid">
Confirm
</Button>
</>
)}
</Drawer>
);
}
122 changes: 122 additions & 0 deletions components/Repositories/AddToWorkspaceModal.tsx
@@ -0,0 +1,122 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { BsGithub } from "react-icons/bs";
import Card from "components/atoms/Card/card";
import SingleSelect from "components/atoms/Select/single-select";
import { Dialog, DialogContent } from "components/molecules/Dialog/dialog";
import Button from "components/shared/Button/button";
import { fetchApiData } from "helpers/fetchApiData";
import useWorkspaces from "lib/hooks/api/useWorkspaces";
import useSupabaseAuth from "lib/hooks/useSupabaseAuth";
import { useToast } from "lib/hooks/useToast";
import Text from "components/atoms/Typography/text";

type AddToWorkspaceModalProps = {
repository: string;
isOpen: boolean;
onCloseModal: () => void;
};

export default function AddToWorkspaceModal({ repository, isOpen, onCloseModal }: AddToWorkspaceModalProps) {
const { toast } = useToast();
const router = useRouter();
const { signIn, user, sessionToken } = useSupabaseAuth();
const [workspaceId, setWorkspaceId] = useState("new");
const { data: workspaces, isLoading: workspacesLoading, mutate } = useWorkspaces({ load: !!user, limit: 100 });

const [host, setHost] = useState("");
useEffect(() => {
if (typeof window !== "undefined") {
setHost(window.location.origin as string);
}
}, []);

const addRepositoryToWorkspace = async () => {
if (workspaceId === "new") {
router.push(`/workspaces/new?repos=${JSON.stringify([repository])}`);
return;
}

const { data, error } = await fetchApiData<Workspace>({
method: "POST",
path: `workspaces/${workspaceId}/repos/${repository}`,
body: {}, // empty body needed to avoid error
bearerToken: sessionToken!,
pathValidator: () => true,
});

if (error) {
toast({ description: `Error adding repository to the workspace. Please try again`, variant: "danger" });
} else {
toast({ description: `Added repository successfully`, variant: "success" });
onCloseModal();
}
};

return (
<Dialog open={isOpen}>
<DialogContent autoStyle={false} onEscapeKeyDown={onCloseModal} onInteractOutside={onCloseModal}>
<Card heading={<h1 className="text-xl font-semibold">Add to workspace</h1>}>
<div className="flex flex-col gap-4 w-[32rem] h-full px-8 py-4">
{!user ? (
<div className="flex flex-col gap-4 text-center">
<img
src="/assets/workspace_overview.png"
alt="Workspace screenshot from documentation"
className="border-2 border-light-orange-9 shadow-md rounded-lg"
/>
<Text>
Keep track of repositories and contributors easily with our new feature
<span className="font-semibold"> Workspaces!</span> If you&apos;ve used OpenSauced before, your
insights and lists are now part of your personal workspace.
</Text>
<p className="font-medium text-light-orange-10">
Create a new workspace with this repository and explore open source like never before!
</p>
<Button
variant="primary"
className="w-fit gap-2 self-center"
onClick={() => {
signIn({
provider: "github",
options: { redirectTo: `${host}/workspaces/new?repos=${JSON.stringify([repository])}` },
});
}}
>
<BsGithub className="w-5 h-5" />
Connect with GitHub
</Button>
</div>
) : (
<>
<p>Create a new workspace or add to an existing one.</p>
{workspacesLoading ? (
<p>Loading...</p>
) : (
<SingleSelect
options={[
{ label: "Create new workspace...", value: "new" },
...workspaces.map(({ id, name }) => ({
label: name,
value: id,
})),
]}
position="popper"
value={workspaceId ?? "new"}
placeholder="Select a workspace"
onValueChange={(value) => {
setWorkspaceId(value);
}}
/>
)}
<Button onClick={addRepositoryToWorkspace} variant="primary" className="w-fit self-end">
Confirm
</Button>
</>
)}
</div>
</Card>
</DialogContent>
</Dialog>
);
}
60 changes: 47 additions & 13 deletions pages/s/[org]/[repo]/index.tsx
@@ -1,9 +1,14 @@
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { HiOutlineExternalLink } from "react-icons/hi";
import { usePostHog } from "posthog-js/react";
import { FiCopy } from "react-icons/fi";
import { MdWorkspaces } from "react-icons/md";
import { HiOutlineExternalLink } from "react-icons/hi";
import dynamic from "next/dynamic";
import { useState } from "react";
import { fetchApiData } from "helpers/fetchApiData";
import { useToast } from "lib/hooks/useToast";
import { shortenUrl } from "lib/utils/shorten-url";
import { useFetchMetricStats } from "lib/hooks/api/useFetchMetricStats";

import ProfileLayout from "layouts/profile";
Expand All @@ -14,11 +19,17 @@ import ClientOnly from "components/atoms/ClientOnly/client-only";
import { DayRangePicker } from "components/shared/DayRangePicker";
import { RepositoryStatCard } from "components/Workspaces/RepositoryStatCard";
import { getRepositoryOgImage, RepositoryOgImage } from "components/Repositories/RepositoryOgImage";
import { useToast } from "lib/hooks/useToast";
import { shortenUrl } from "lib/utils/shorten-url";
import Button from "components/shared/Button/button";
import { getAvatarByUsername } from "lib/utils/github";
import { useRepoStats } from "lib/hooks/api/useRepoStats";
import { useMediaQuery } from "lib/hooks/useMediaQuery";

const AddToWorkspaceModal = dynamic(() => import("components/Repositories/AddToWorkspaceModal"), {
ssr: false,
});
const AddToWorkspaceDrawer = dynamic(() => import("components/Repositories/AddToWorkspaceDrawer"), {
ssr: false,
});

export async function getServerSideProps(context: GetServerSidePropsContext) {
const { org, repo } = context.params ?? { org: "", repo: "" };
Expand Down Expand Up @@ -58,6 +69,7 @@ export default function RepoPage({ repoData, ogImageUrl }: RepoPageProps) {
const avatarUrl = getAvatarByUsername(repoData.full_name.split("/")[0], 96);
const { toast } = useToast();
const posthog = usePostHog();
const isMobile = useMediaQuery("(max-width: 576px)");

const syncId = repoData.id;
const router = useRouter();
Expand Down Expand Up @@ -87,6 +99,8 @@ export default function RepoPage({ repoData, ogImageUrl }: RepoPageProps) {
const starsRangedTotal = starsData?.reduce((prev, curr) => prev + curr.star_count!, 0);
const forksRangedTotal = forkStats?.reduce((prev, curr) => prev + curr.forks_count!, 0);

const [isAddToWorkspaceModalOpen, setIsAddToWorkspaceModalOpen] = useState(false);

const copyUrlToClipboard = async () => {
const url = new URL(window.location.href).toString();
posthog!.capture("clicked: repo page share button", {
Expand Down Expand Up @@ -123,16 +137,30 @@ export default function RepoPage({ repoData, ogImageUrl }: RepoPageProps) {
<p className="md:text-xl">{repoData.description}</p>
</div>
</header>
<div className="self-end flex gap-2 items-center">
<DayRangePicker />
<Button
variant="outline"
onClick={copyUrlToClipboard}
className="my-auto gap-2 items-center shrink-0 place-self-end"
>
<FiCopy />
Share
</Button>
<div className="self-end flex flex-col gap-2 items-end">
{isMobile ? (
<AddToWorkspaceDrawer repository={repoData.full_name} />
) : (
<Button
variant="primary"
onClick={() => setIsAddToWorkspaceModalOpen(true)}
className="shrink-0 items-center gap-3 w-fit"
>
<MdWorkspaces />
Add to Workspace
</Button>
)}
<div className="flex gap-2 items-center">
<Button
variant="outline"
onClick={copyUrlToClipboard}
className="my-auto gap-2 items-center shrink-0 place-self-end"
>
<FiCopy />
Share
</Button>
<DayRangePicker />
</div>
</div>
</div>
<ClientOnly>
Expand Down Expand Up @@ -207,6 +235,12 @@ export default function RepoPage({ repoData, ogImageUrl }: RepoPageProps) {
</ClientOnly>
</section>
</ProfileLayout>

<AddToWorkspaceModal
repository={repoData.full_name}
isOpen={isAddToWorkspaceModalOpen}
onCloseModal={() => setIsAddToWorkspaceModalOpen(false)}
/>
</>
);
}

0 comments on commit b1efc46

Please sign in to comment.