GitFOSS
feat(repository): continue work on Repository*View to display usefull data
+ 357
- 67
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663515923444,
+  "_generatedAtUnix": 1663685370478,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -15,6 +15,18 @@
       "pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"
     },
+    "RepositoryInitialSetup": {
+      "hash": "4916d0555f98e2c8438fea37315212d6d538c9a8",
+      "pathSource": "./app/islands/RepositoryInitialSetup.tsx",
+      "pathBundle": "./public/.islands/RepositoryInitialSetup.bundle.js",
+      "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
+    },
+    "RepositoryTreeView": {
+      "hash": "e5a3555080f0a865e31011f45a376c4a0e78a2c7",
+      "pathSource": "./app/islands/RepositoryTreeView.tsx",
+      "pathBundle": "./public/.islands/RepositoryTreeView.bundle.js",
+      "pathSourceMap": "./public/.islands/RepositoryTreeView.bundle.js.map"
+    },
     "SideMenu": {
       "hash": "5d01374da1cbee58e081b9022aa56f0624209e27",
       "pathSource": "./app/islands/SideMenu.tsx",

...
@@ -48,11 +60,11 @@
       "pathSource": "./app/views/repository/RepositoryCreateView.tsx"
     },
     "RepositoryDetailsView": {
-      "hash": "671bd0202822bb20af517af1bfee98e138cd61ac",
+      "hash": "43ee50554a5b3adaf6ab010a62816f4bb9d32d68",
       "pathSource": "./app/views/repository/RepositoryDetailsView.tsx"
     },
     "RepositoryExploreView": {
-      "hash": "53c3c318d1f6dd198a9627e9024dfc5737091a80",
+      "hash": "c4c1d08c876232054af3318739a6d1091510afb2",
       "pathSource": "./app/views/repository/RepositoryExploreView.tsx"
     }
   }

app/components/PageWrapper.tsx
@@ -11,5 +11,25 @@ export const PageWrapper = styled.div`
 
   margin: 0 auto;
   padding: 24px 16px 64px 16px;
-  gap: 24px;
+
+  & > h1 {
+    margin: 0;
+    &:not(:first-of-type) {
+      margin-top: 32px;
+    }
+  }
+
+  & > h2 {
+    margin: 0;
+    &:not(:first-of-type) {
+      margin-top: 24px;
+    }
+  }
+
+  & > p {
+    margin: 0;
+    &:not(:first-of-type) {
+      margin-top: 16px;
+    }
+  }
 `;

app/controllers/repository/getRepositoryDetailsView.ts
@@ -3,6 +3,7 @@ import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
 import { AppRoute, AppRoutesParams } from "../../routes";
 // app services
+import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";
 import { makeUsersService } from "../../services/user";
 // app views

...
@@ -14,8 +15,9 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug } =
     request.params as AppRoutesParams[AppRoute.REPOSITORY_DETAILS]["params"];
 
-  const usersService = makeUsersService({ request });
+  const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });
+  const usersService = makeUsersService({ request });
 
   const currentUser =
     request.session.data.authenticated &&

...
@@ -23,6 +25,7 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
       ? await usersService.getUserById(request.session.data.curr_user_uid)
       : null;
 
+  const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const ref = "HEAD";
   const repo = await repoService.getRepository(orgSlug, repoSlug);
   if (repo == null) {

...
@@ -38,6 +41,7 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
         http: await repoService.getRepositoryHTTPCloneUrl(repo),
         ssh: await repoService.getRepositorySSHCloneUrl(repo),
       },
+      parentOrg,
       ref,
       repo,
       repoHead: await repoService.getRepositoryHead(repo, ref),

...
@@ -54,6 +58,7 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
             http: await repoService.getRepositoryHTTPCloneUrl(repo),
             ssh: await repoService.getRepositorySSHCloneUrl(repo),
           },
+          parentOrg,
           ref,
           repo,
           repoHead: null,

new file
app/islands/RepositoryInitialSetup.tsx
@@ -0,0 +1,65 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+import styled from "styled-components";
+// generated via script[generate:prisma]
+import { Repository, User } from "@prisma/client";
+
+export interface RepositoryInitialSetupProps {
+  currentUser: null | User;
+  cloneUrl: {
+    http: string;
+    ssh: string;
+  };
+  ref: string;
+  repo: Repository;
+}
+
+const RepositoryInitialSetup: ReactIsland<RepositoryInitialSetupProps> = ({
+  cloneUrl,
+  currentUser,
+  repo,
+}) => {
+  return (
+    <StyledRepositoryInitialSetupContainer>
+      <p>It looks like this repository is empty.</p>
+      <p>Get started easily:</p>
+      <h3>Clone and initialize</h3>
+      <code>
+        <pre
+          style={{ maxWidth: 600 }}
+        >{`# Clone and enter the repository directory
+      $ git clone ${cloneUrl.http}
+      $ cd ${repo.slug}/
+      ${
+        currentUser != null
+          ? `
+      # Setup committer identity for this project
+      $ git config user.name "${
+        currentUser.displayName || currentUser.username
+      }"
+      $ git config user.email "${currentUser.email}"`
+          : ""
+      }
+
+      # Create some base files
+      $ echo "# ${repo.displayName || repo.slug}" > README.md
+      $ echo "The MIT License" > LICENSE
+
+      # Track files, commit and send to GitFOSS remote repository
+      $ git add .
+      $ git commit -am 'feat: initial commit'
+      $ git push
+      `}</pre>
+      </code>
+    </StyledRepositoryInitialSetupContainer>
+  );
+};
+
+const StyledRepositoryInitialSetupContainer = styled.div`
+  width: 100%;
+`;
+
+RepositoryInitialSetup.displayName = "RepositoryInitialSetup";
+export default RepositoryInitialSetup;

new file
app/islands/RepositoryTreeView.tsx
@@ -0,0 +1,75 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React, { useCallback } from "react";
+import styled from "styled-components";
+// app
+import { RepositoryFile, RepositoryHead } from "../types";
+
+export interface RepositoryTreeViewProps {
+  currPath: string;
+  orgSlug: string;
+  repoHead: RepositoryHead;
+  repoFiles: RepositoryFile[];
+  repoSlug: string;
+}
+
+const RepositoryTreeView: ReactIsland<RepositoryTreeViewProps> = ({
+  currPath,
+  orgSlug,
+  repoFiles,
+  repoHead,
+  repoSlug,
+}) => {
+  const buildRepoFileLink = useCallback(
+    (file: RepositoryFile) => {
+      const fileName = `${file.name}${file.type === "tree" ? "/" : ""}`;
+      return {
+        text: fileName,
+        href:
+          currPath === "/"
+            ? `/${orgSlug}/${repoSlug}/main/tree/${fileName}`
+            : `/${orgSlug}/${repoSlug}/main/tree/${currPath}/${fileName}`,
+      };
+    },
+    [orgSlug, repoSlug, currPath]
+  );
+
+  return (
+    <StyledRepositoryTreeViewContainer>
+      <div>
+        <strong>{repoHead.author.name}</strong>
+        {" ∙ "}
+        <span>{repoHead.commitMessage}</span>
+        {" - "}
+        <span>
+          {repoHead.treeId.substring(0, 8)}
+          {repoHead.parentId
+            ? ` ∙ parent ${repoHead.parentId.substring(0, 8)}`
+            : ""}
+        </span>
+        {" ∙ "}
+        <span>{new Date(repoHead.author.timestamp * 1000).toUTCString()}</span>
+      </div>
+      <div>
+        <ul>
+          {repoFiles.map((file) => {
+            const fileLink = buildRepoFileLink(file);
+            return (
+              <li key={file.id}>
+                <a href={fileLink.href}>{fileLink.text}</a>
+              </li>
+            );
+          })}
+        </ul>
+      </div>
+    </StyledRepositoryTreeViewContainer>
+  );
+};
+
+const StyledRepositoryTreeViewContainer = styled.div`
+  width: 100%;
+`;
+
+RepositoryTreeView.displayName = "RepositoryTreeView";
+export default RepositoryTreeView;

new file
app/services/organization/getOrganizationById.ts
@@ -0,0 +1,25 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { Organization, Prisma } from "@prisma/client";
+// service
+import { OrganizationServiceDeps } from "./types";
+
+const makeGetOrganizationById: ServiceMethodFactory<
+  OrganizationServiceDeps,
+  [string, Prisma.OrganizationInclude | undefined],
+  Promise<Organization | null>
+> = ({ request }) => {
+  return async (orgId, include = undefined) => {
+    const organization = await request.prisma.organization.findUnique({
+      include,
+      where: {
+        id: orgId,
+      },
+    });
+
+    return organization;
+  };
+};
+
+export default makeGetOrganizationById;

new file
app/services/organization/getOrganizationBySlug.ts
@@ -0,0 +1,25 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { Organization, Prisma } from "@prisma/client";
+// service
+import { OrganizationServiceDeps } from "./types";
+
+const makeGetOrganizationBySlug: ServiceMethodFactory<
+  OrganizationServiceDeps,
+  [string, Prisma.OrganizationInclude | undefined],
+  Promise<Organization | null>
+> = ({ request }) => {
+  return async (orgSlug, include = undefined) => {
+    const organization = await request.prisma.organization.findUnique({
+      include,
+      where: {
+        slug: orgSlug,
+      },
+    });
+
+    return organization;
+  };
+};
+
+export default makeGetOrganizationBySlug;

new file
app/services/organization/index.ts
@@ -0,0 +1,15 @@
+// 1st-party
+import { makeService } from "@ethicdevs/react-monolith";
+// service
+import type { OrganizationServiceAPI, OrganizationServiceDeps } from "./types";
+// service methods
+import { default as makeGetOrganizationById } from "./getOrganizationById";
+import { default as makeGetOrganizationBySlug } from "./getOrganizationBySlug";
+
+export const makeOrganizationService = makeService<
+  OrganizationServiceAPI,
+  OrganizationServiceDeps
+>({
+  getOrganizationById: makeGetOrganizationById,
+  getOrganizationBySlug: makeGetOrganizationBySlug,
+});

new file
app/services/organization/types.ts
@@ -0,0 +1,22 @@
+// 1st-party
+import { ServiceApiContract } from "@ethicdevs/react-monolith";
+// 3rd-party
+import { FastifyRequest } from "fastify";
+// generated via script[generate:prisma]
+import { Organization, Prisma } from "@prisma/client";
+
+// service
+export interface OrganizationServiceAPI extends ServiceApiContract {
+  getOrganizationById(
+    orgId: string,
+    include?: Prisma.OrganizationInclude
+  ): Promise<Organization | null>;
+  getOrganizationBySlug(
+    orgSlug: string,
+    include?: Prisma.OrganizationInclude
+  ): Promise<Organization | null>;
+}
+
+export interface OrganizationServiceDeps {
+  request: FastifyRequest;
+}

app/services/repository/getRepositoryExploreCollection.ts
@@ -1,7 +1,7 @@
 // 1st-party
 import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
-import { Repository, ResourceVisibility } from "@prisma/client";
+import { Organization, Repository, ResourceVisibility } from "@prisma/client";
 // service
 import type { RepositoryServiceDeps } from "./types";
 import { default as makeGetRepositoryHTTPCloneUrl } from "./getRepositoryHTTPCloneUrl";

...
@@ -10,21 +10,25 @@ import { default as makeGetRepositorySSHCloneUrl } from "./getRepositorySSHClone
 const makeGetRepositoryExploreCollection: ServiceMethodFactory<
   RepositoryServiceDeps,
   void[],
-  Promise<Repository[]>
+  Promise<(Repository & { parentOrg: Organization })[]>
 > = (deps) => {
   const { request } = deps;
   const getRepositoryHTTPCloneUrl = makeGetRepositoryHTTPCloneUrl(deps);
   const getRepositorySSHCloneUrl = makeGetRepositorySSHCloneUrl(deps);
   return async () => {
     const repositories = await request.prisma.repository.findMany({
+      include: {
+        organization: true,
+      },
       where: {
         visibility: ResourceVisibility.PUBLIC,
       },
     });
 
     const repositoriesWithMetas = await Promise.all(
-      repositories.map(async (repo) => ({
+      repositories.map(async ({ organization: parentOrg, ...repo }) => ({
         ...repo,
+        parentOrg,
         httpCloneUrl: await getRepositoryHTTPCloneUrl(repo),
         sshCloneUrl: await getRepositorySSHCloneUrl(repo),
       }))

app/services/repository/types.ts
@@ -5,7 +5,7 @@ import type { ServiceApiContract } from "@ethicdevs/react-monolith";
 // 3rd-party
 import type { FastifyRequest } from "fastify";
 // generated via script[generate:prisma]
-import type { Repository } from "@prisma/client";
+import type { Organization, Repository } from "@prisma/client";
 // app
 import type { RepositoryFile, RepositoryHead } from "../../types";
 

...
@@ -27,7 +27,9 @@ export interface CreateRepositoryDTO {
 export interface RepositoryServiceAPI extends ServiceApiContract {
   createRepository(dto: CreateRepositoryDTO): Promise<Repository>;
   getRepository(orgSlug: string, repoSlug: string): Promise<Repository | null>;
-  getRepositoryExploreCollection(): Promise<Repository[]>;
+  getRepositoryExploreCollection(): Promise<
+    (Repository & { parentOrg: Organization })[]
+  >;
   getRepositoryFiles(
     repository: Repository,
     ref?: string

app/views/repository/RepositoryDetailsView.tsx
@@ -3,10 +3,13 @@ import type { ReactView } from "@ethicdevs/react-monolith";
 // 3rd-party
 import React from "react";
 // generated via script[prisma:generate]
-import { Repository, User } from "@prisma/client";
+import type { Organization, Repository, User } from "@prisma/client";
 // app
 import type { CommonProps, RepositoryHead, RepositoryFile } from "../../types";
 import { Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoryInitialSetup from "../../islands/RepositoryInitialSetup";
+import RepositoryTreeView from "../../islands/RepositoryTreeView";
 
 export interface RepositoryDetailsViewProps extends CommonProps {
   currentUser: null | User;

...
@@ -14,6 +17,7 @@ export interface RepositoryDetailsViewProps extends CommonProps {
     http: string;
     ssh: string;
   };
+  parentOrg: Organization;
   ref: string;
   repo: Repository;
   repoHead: null | RepositoryHead;

...
@@ -24,6 +28,7 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
   currentUser,
   commonProps,
   cloneUrl,
+  parentOrg,
   ref,
   repo,
   repoHead,

...
@@ -32,57 +37,64 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
   return (
     <Layout {...commonProps} showSideMenu={false}>
       <PageWrapper>
-        <h1>{ref}</h1>
-        <code>
-          <pre style={{ maxWidth: 600 }}>{JSON.stringify(repo, null, 2)}</pre>
-        </code>
+        <h1>
+          {parentOrg.displayName || parentOrg.slug}
+          {" / "}
+          {repo.displayName || repo.slug}
+          {" ∙ "}
+          <span style={{ textTransform: "capitalize" }}>
+            ({repo.visibility.toLowerCase()})
+          </span>
+        </h1>
+        <div>
+          <p>{repo.shortDescription}</p>
+          {repo.websiteUrl != null && (
+            <p>
+              <a
+                href={repo.websiteUrl}
+                target={"_blank"}
+                rel={"noopener noreferer noreferrer"}
+              >
+                {repo.websiteUrl}
+              </a>
+            </p>
+          )}
+        </div>
+        <div>
+          {repo.keywords.map((keyword, idx, arr) => (
+            <React.Fragment key={[idx, keyword].join(":")}>
+              <span>{keyword}</span>
+              {idx < arr.length - 1 ? ", " : "."}
+            </React.Fragment>
+          ))}
+        </div>
+        <div>
+          <p>
+            HTTP Clone: <code>{cloneUrl.http}</code>
+          </p>
+          <p>
+            SSH Clone: <code>{cloneUrl.ssh}</code>
+          </p>
+        </div>
         {repoHead == null ? (
-          <div>
-            <p>It looks like this repository is empty.</p>
-            <p>Get started easily:</p>
-            <h3>Clone and initialize</h3>
-            <code>
-              <pre
-                style={{ maxWidth: 600 }}
-              >{`# Clone and enter the repository directory
-$ git clone ${cloneUrl.http}
-$ cd ${repo.slug}/
-${
-  currentUser != null
-    ? `
-# Setup committer identity for this project
-$ git config user.name "${currentUser.displayName || currentUser.username}"
-$ git config user.email "${currentUser.email}"`
-    : ""
-}
-
-# Create some base files
-$ echo "# ${repo.displayName || repo.slug}" > README.md
-$ echo "The MIT License" > LICENSE
-
-# Track files, commit and send to GitFOSS remote repository
-$ git add .
-$ git commit -am 'feat: initial commit'
-$ git push
-`}</pre>
-            </code>
+          <div data-islandid={`${RepositoryInitialSetup.name}$$0`}>
+            <RepositoryInitialSetup
+              cloneUrl={cloneUrl}
+              currentUser={currentUser}
+              ref={ref}
+              repo={repo}
+            />
           </div>
         ) : (
-          <>
-            <code>
-              <pre style={{ maxWidth: 600 }}>
-                {JSON.stringify(repoHead, null, 2)}
-              </pre>
-            </code>
-            {repoFiles.map((file) => (
-              <div key={file.id}>
-                <code>
-                  {file.name}
-                  {file.type === "tree" ? "/" : ""}
-                </code>
-              </div>
-            ))}
-          </>
+          <div data-islandid={`${RepositoryTreeView.name}$$0`}>
+            <RepositoryTreeView
+              currPath={"/"}
+              orgSlug={parentOrg.slug}
+              repoHead={repoHead}
+              repoFiles={repoFiles}
+              repoSlug={repo.slug}
+            />
+          </div>
         )}
       </PageWrapper>
     </Layout>

app/views/repository/RepositoryExploreView.tsx
@@ -3,13 +3,13 @@ import type { ReactView } from "@ethicdevs/react-monolith";
 // 3rd-party
 import React from "react";
 // generated via script[generate:prisma]
-import type { Repository } from "@prisma/client";
+import type { Organization, Repository } from "@prisma/client";
 // app
 import type { CommonProps } from "../../types";
 import { Layout, PageWrapper } from "../../components";
 
 export interface RepositoryExploreViewProps extends CommonProps {
-  repositories: Repository[];
+  repositories: (Repository & { parentOrg: Organization })[];
 }
 
 const RepositoryExploreView: ReactView<RepositoryExploreViewProps> = ({

...
@@ -21,12 +21,20 @@ const RepositoryExploreView: ReactView<RepositoryExploreViewProps> = ({
       <PageWrapper>
         {repositories.map((repo) => (
           <div key={repo.id}>
-            <h2>{repo.displayName}</h2>
-            <code>
-              <pre style={{ maxWidth: 600 }}>
-                {JSON.stringify(repo, null, 2)}
-              </pre>
-            </code>
+            <h1>
+              <a href={`/${repo.parentOrg.slug}/${repo.slug}`}>
+                {repo.parentOrg.displayName || repo.parentOrg.slug}
+                {" / "}
+                {repo.displayName || repo.slug}
+                {" ∙ "}
+                <span style={{ textTransform: "capitalize" }}>
+                  ({repo.visibility.toLowerCase()})
+                </span>
+              </a>
+            </h1>
+            <div>
+              <p>{repo.shortDescription}</p>
+            </div>
           </div>
         ))}
       </PageWrapper>