feat(repository): add a "RepositoryCreateForm" island that is ugly but functional yet
+ 363
- 12
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663457839996,
+  "_generatedAtUnix": 1663465895278,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -9,6 +9,12 @@
       "pathBundle": "./public/.islands/InstantRouterIndicator.bundle.js",
       "pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
     },
+    "RepositoryCreateForm": {
+      "hash": "538d1b96e2d3672a2071c79aae841b71dd3f9975",
+      "pathSource": "./app/islands/RepositoryCreateForm.tsx",
+      "pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
+      "pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"
+    },
     "SideMenu": {
       "hash": "5d01374da1cbee58e081b9022aa56f0624209e27",
       "pathSource": "./app/islands/SideMenu.tsx",

...
@@ -37,6 +43,10 @@
       "hash": "3e2c7053b529624ed6cfa9ea8631b4480bd9775b",
       "pathSource": "./app/views/auth/RegisterView.tsx"
     },
+    "RepositoryCreateView": {
+      "hash": "c3f2c6e08686690cb466628aa7d0715e99d9ba70",
+      "pathSource": "./app/views/repository/RepositoryCreateView.tsx"
+    },
     "RepositoryExploreView": {
       "hash": "53c3c318d1f6dd198a9627e9024dfc5737091a80",
       "pathSource": "./app/views/repository/RepositoryExploreView.tsx"

new file
app/components/Grid.tsx
@@ -0,0 +1,43 @@
+import styled, { css, CSSProperties, StyledComponent } from "styled-components";
+
+type GridCommonTypes = {
+  /**
+   * If set, will flex: 1.
+   * Takes precedence over `flex`.
+   */
+  fluid?: boolean;
+  flex?: CSSProperties["flex"];
+  gap?: CSSProperties["gap"];
+  alignItems?: CSSProperties["alignItems"];
+  justifyContent?: CSSProperties["justifyContent"];
+  nowrap?: boolean;
+};
+
+type GridTypes = {
+  Row: StyledComponent<"div", object, GridCommonTypes, never>;
+  Col: StyledComponent<"div", object, GridCommonTypes, never>;
+};
+
+const gridCommonCss = css<GridCommonTypes>`
+  display: flex;
+  justify-content: ${({ justifyContent }) => justifyContent || "flex-start"};
+  align-items: ${({ alignItems }) => alignItems || "flex-start"};
+
+  width: 100%;
+  height: 100%;
+
+  flex: ${({ flex, fluid }) => (fluid ? 1 : flex || 0)};
+  gap: ${({ gap }) =>
+    gap != null ? (typeof gap === "number" ? `${gap}px` : gap) : "auto"};
+`;
+
+export const Grid: GridTypes = {
+  Row: styled.div<GridCommonTypes>`
+    ${gridCommonCss};
+    flex-flow: row ${({ nowrap }) => (nowrap ? "nowrap" : "wrap")};
+  `,
+  Col: styled.div<GridCommonTypes>`
+    ${gridCommonCss};
+    flex-flow: column ${({ nowrap }) => (nowrap ? "nowrap" : "wrap")};
+  `,
+};

app/components/PageHeader.tsx
@@ -5,11 +5,9 @@ import styled, { css } from "styled-components";
 import type { CommonProps, WithThemeSchemeProp } from "../types";
 import { Const } from "../const";
 import { NamedColors } from "../utils/style";
-// import { ReactMonolithLogo } from "./icons";
 
 interface PageHeaderProps extends CommonProps {}
 
-// const LOGO_HEIGHT = 36;
 const SIDE_MENU_WIDTH = 320;
 
 export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({

...
@@ -21,9 +19,11 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
   const pageHeaderActions = useMemo(() => {
     if (commonProps.authenticated) {
       return (
-        <a aria-label={"Log off your account"} href={"/auth/logout"}>
-          Logout
-        </a>
+        <>
+          <a aria-label={"Log off your account"} href={"/auth/logout"}>
+            Logout
+          </a>
+        </>
       );
     }
 

...
@@ -43,16 +43,20 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
     <StyledPageHeader themeScheme={themeScheme}>
       <StyledLogoArea themeScheme={themeScheme}>
         <a href={"/"}>
-          {/*<ReactMonolithLogo size={LOGO_HEIGHT} />*/}
           <h1 style={{ margin: 0, marginLeft: 20 }}>{Const.APP_NAME}</h1>
         </a>
       </StyledLogoArea>
       <StyledPageHeaderNav>
-        <a aria-label={"Explore Repositories"} href={"/repositories/explore"}>
+        <a aria-label={"Explore Repositories"} href={"/repo/explore"}>
           Explore Repositories
         </a>
       </StyledPageHeaderNav>
       <StyledActionsArea>
+        {commonProps.authenticated && (
+          <a aria-label={"Create a new Repository"} href={"/repo/new"}>
+            New Repository
+          </a>
+        )}
         <a
           href={`/theme/${invertThemeScheme}`}
           style={{ color: NamedColors.TEXT_MUTED[themeScheme] }}

...
@@ -80,11 +84,12 @@ const StyledPageHeader = styled.header<WithThemeSchemeProp>`
   gap: 16px;
 
   & a {
+    transition: color 140ms ease-in-out 0s;
+    text-decoration: none;
+
     ${({ themeScheme }) => css`
       color: ${NamedColors.TEXT_MUTED[themeScheme]};
     `};
-    transition: color 140ms ease-in-out 0s;
-    text-decoration: none;
 
     &:hover {
       ${({ themeScheme }) => css`

...
@@ -104,6 +109,7 @@ const StyledLogoArea = styled.div<WithThemeSchemeProp>`
     flex-flow: row nowrap;
     justify-content: flex-start;
     align-items: center;
+
     ${({ themeScheme }) => css`
       color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
     `};

app/components/index.ts
@@ -1,5 +1,6 @@
 export { Button, ButtonAnchor } from "./Button.styled";
 export { Card } from "./Card.styled";
+export { Grid } from "./Grid";
 export * as Icons from "./icons";
 export { Layout } from "./Layout";
 export { MarkdownToJsx } from "./MarkdownToJsx";

new file
app/controllers/repository/getRepositoryCreateView.ts
@@ -0,0 +1,18 @@
+// 1st-party
+import { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+// import { makeRepositoryService } from "../../services/repository";
+// app views
+import RepositoryCreateView, {
+  RepositoryCreateViewProps,
+} from "../../views/repository/RepositoryCreateView";
+
+const getRepositoryCreateView: ReqHandler = async (request, reply) => {
+  // const repoService = makeRepositoryService({ request });
+  const reqHandler = reply.makeRequestHandler(request, reply);
+  return reqHandler<RepositoryCreateViewProps>(RepositoryCreateView.name, {
+    availableParentOrgs: [],
+  });
+};
+
+export default getRepositoryCreateView;

app/controllers/repository/index.ts
@@ -1,5 +1,7 @@
+import { default as getRepositoryCreateView } from "./getRepositoryCreateView";
 import { default as getRepositoryExploreView } from "./getRepositoryExploreView";
 
 export const RepositoryController = {
+  getRepositoryCreateView,
   getRepositoryExploreView,
 };

new file
app/islands/RepositoryCreateForm.tsx
@@ -0,0 +1,209 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React, { useCallback, useState } from "react";
+// generated via script[generate:prisma]
+import type { Organization } from "@prisma/client";
+// app
+import { Grid } from "../components/Grid";
+
+export interface RepositoryCreateFormProps {
+  availableParentOrgs: Organization[];
+  editMode?: boolean;
+  initialValues?: {
+    parent_org_slug?: string;
+    repo_slug?: string;
+    repo_display_name?: string;
+    repo_short_description?: string;
+    repo_website_url?: string;
+    repo_keywords?: string[];
+  };
+}
+
+const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
+  availableParentOrgs,
+  editMode = false,
+  initialValues = undefined,
+}) => {
+  const [keywords, setKeywords] = useState<string[]>([
+    ...(initialValues?.repo_keywords || []),
+  ]);
+  const [repoInitLicenseFileChecked, setRepoInitLicenseFileChecked] =
+    useState<boolean>(false);
+
+  editMode;
+
+  const onKeywordsInputChange = useCallback(
+    (ev: React.ChangeEvent<HTMLInputElement>) => {
+      if (ev == null || ev.target == null || ev.target.value == null) {
+        return undefined;
+      }
+      setKeywords(ev.target.value.split(",").map((w) => w.trim()));
+      return undefined;
+    },
+    [setKeywords]
+  );
+
+  const onRepoInitLicenseFileChange = useCallback(
+    (ev: React.ChangeEvent<HTMLInputElement>) => {
+      if (ev == null || ev.target == null || ev.target.value == null) {
+        return undefined;
+      }
+      setRepoInitLicenseFileChecked(ev.target.checked);
+      return undefined;
+    },
+    [setRepoInitLicenseFileChecked]
+  );
+
+  return (
+    <div>
+      <fieldset>
+        <legend>Repository details</legend>
+        {/* Parent Organization Select */}
+        <Grid.Col fluid nowrap>
+          <label htmlFor={"repo_display_name"}>
+            Owner Organization <span>(*)</span>:
+          </label>
+          <select
+            style={styles.inputMaxWidth}
+            defaultValue={initialValues?.parent_org_slug}
+            name={"parent_org_slug"}
+            required
+          >
+            {availableParentOrgs.map((org) => (
+              <option key={org.id} value={org.slug}>
+                {org.displayName || org.slug}
+              </option>
+            ))}
+          </select>
+        </Grid.Col>
+        {/* Repository Name */}
+        <Grid.Col fluid nowrap>
+          <label htmlFor={"repo_display_name"}>
+            Repository Name <span>(*)</span>:
+          </label>
+          <input
+            style={styles.inputMaxWidth}
+            defaultValue={initialValues?.repo_display_name}
+            name={"repo_display_name"}
+            placeholder={"i.e. My Super Project"}
+            required
+            type={"text"}
+          />
+        </Grid.Col>
+        {/* Repository Slug */}
+        <Grid.Col fluid nowrap>
+          <label htmlFor={"repo_slug"}>
+            Repository Slug <span>(*)</span>:
+          </label>
+          <input
+            style={styles.inputMaxWidth}
+            defaultValue={initialValues?.repo_slug}
+            name={"repo_slug"}
+            placeholder={"i.e. my-super-project"}
+            required
+            type={"text"}
+          />
+        </Grid.Col>
+      </fieldset>
+      <fieldset>
+        <legend>Repository description</legend>
+        {/* Short Description */}
+        <Grid.Col fluid nowrap>
+          <label htmlFor={"repo_short_description"}>Short Description:</label>
+          <textarea
+            style={styles.inputMaxWidth}
+            defaultValue={initialValues?.repo_short_description}
+            name={"repo_short_description"}
+            placeholder={"i.e. A super project about things that are super!"}
+          ></textarea>
+        </Grid.Col>
+        {/* Website URL */}
+        <Grid.Col fluid nowrap>
+          <label htmlFor={"repo_website_url"}>Website URL:</label>
+          <input
+            style={styles.inputMaxWidth}
+            defaultValue={initialValues?.repo_website_url}
+            name={"repo_website_url"}
+            placeholder={"i.e. https://www.super-project.com"}
+            type={"text"}
+          />
+        </Grid.Col>
+        {/* Keywords */}
+        <Grid.Col fluid nowrap>
+          <label htmlFor={"repo_keywords_add"}>Keywords:</label>
+          <input type={"hidden"} name={"repo_keywords"} value={keywords} />
+          <input
+            style={styles.inputMaxWidth}
+            name={"repo_keywords_add"}
+            placeholder={"Keywords separated by a coma (,)..."}
+            type={"text"}
+            onChange={onKeywordsInputChange}
+          />
+          {keywords.map((word, idx) => (
+            <input
+              style={styles.inputMaxWidth}
+              key={[idx, word].join(":")}
+              disabled
+              type={"text"}
+              value={word}
+            />
+          ))}
+        </Grid.Col>
+      </fieldset>
+      <fieldset>
+        <legend>Repository setup</legend>
+        {/* Initialise Read Me file? */}
+        <Grid.Row fluid nowrap>
+          <label htmlFor={"repo_init_readme_file"} style={styles.inputMaxWidth}>
+            Initialize with empty README.md file?
+          </label>
+          <input
+            type={"checkbox"}
+            name={"repo_init_readme_file"}
+            defaultChecked={false}
+          />
+        </Grid.Row>
+        <Grid.Row fluid nowrap>
+          {/* Initialise License file? */}
+          <label htmlFor={"repo_init_readme_file"} style={styles.inputMaxWidth}>
+            Initialize with a LICENSE file?
+          </label>
+          <select
+            disabled={repoInitLicenseFileChecked === false}
+            name={"repo_init_license_kind"}
+            defaultValue={"MIT"}
+          >
+            <option key={"license:mit"} value={"mit"}>
+              MIT License
+            </option>
+            <option key={"license:gnu-gpl-v3"} value={"gnu-gpl-v3"}>
+              GNU GPL v.3.0
+            </option>
+            <option key={"license:gnu-agpl-v3"} value={"gnu-agpl-v3"}>
+              AGPL v.3.0
+            </option>
+            <option key={"license:gnu-lgpl-v3"} value={"gnu-lgpl-v3"}>
+              LGPL v.3.0
+            </option>
+          </select>
+          <input
+            type={"checkbox"}
+            name={"repo_init_license_file"}
+            onChange={onRepoInitLicenseFileChange}
+            checked={repoInitLicenseFileChecked}
+          />
+        </Grid.Row>
+      </fieldset>
+    </div>
+  );
+};
+
+const styles = {
+  inputMaxWidth: {
+    width: "100%",
+  },
+};
+
+RepositoryCreateForm.displayName = "RepositoryCreateForm";
+export default RepositoryCreateForm;

@@ -156,10 +156,16 @@ const RootAppRouter: AppRouter = () => {
         <Router.Route
           name={AppRoute.REPOSITORY_EXPLORE}
           method={"GET"}
-          path={"/repositories/explore"}
-          preHandler={authenticatedOrLogin()}
+          path={"/repo/explore"}
           handler={RepositoryController.getRepositoryExploreView}
         />
+        <Router.Route
+          name={AppRoute.REPOSITORY_EXPLORE}
+          method={"GET"}
+          path={"/repo/new"}
+          preHandler={authenticatedOrLogin()}
+          handler={RepositoryController.getRepositoryCreateView}
+        />
       </Router.Group>
     </Router.Root>
   );

new file
app/views/repository/RepositoryCreateView.tsx
@@ -0,0 +1,56 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import { Organization } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Button, Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoryCreateForm from "../../islands/RepositoryCreateForm";
+
+export interface RepositoryCreateViewProps extends CommonProps {
+  availableParentOrgs: Organization[];
+  errorMessage?: null | string;
+  initialValues?: {
+    parent_org_slug?: string;
+    repo_slug?: string;
+    repo_display_name?: string;
+    repo_short_description?: string;
+    repo_website_url?: string;
+    repo_keywords?: string[];
+  };
+}
+
+const RepositoryCreateView: ReactView<RepositoryCreateViewProps> = ({
+  availableParentOrgs,
+  commonProps,
+  errorMessage = undefined,
+  initialValues = undefined,
+}) => {
+  return (
+    <Layout {...commonProps} showSideMenu={false}>
+      <PageWrapper>
+        {errorMessage && (
+          <div className={"error_message"}>
+            <p>{errorMessage}</p>
+          </div>
+        )}
+        <form action={`/repo/new`} method={"POST"}>
+          <div data-islandid={`${RepositoryCreateForm.name}$$0`}>
+            <RepositoryCreateForm
+              availableParentOrgs={availableParentOrgs}
+              initialValues={initialValues}
+            />
+          </div>
+          {/* Submit Button */}
+          <Button type={"submit"}>Create Repository</Button>
+        </form>
+      </PageWrapper>
+    </Layout>
+  );
+};
+
+RepositoryCreateView.displayName = "RepositoryCreateView";
+export default RepositoryCreateView;

app/routes.tsx
@@ -156,10 +156,16 @@ const RootAppRouter: AppRouter = () => {
         <Router.Route
           name={AppRoute.REPOSITORY_EXPLORE}
           method={"GET"}
-          path={"/repositories/explore"}
-          preHandler={authenticatedOrLogin()}
+          path={"/repo/explore"}
           handler={RepositoryController.getRepositoryExploreView}
         />
+        <Router.Route
+          name={AppRoute.REPOSITORY_EXPLORE}
+          method={"GET"}
+          path={"/repo/new"}
+          preHandler={authenticatedOrLogin()}
+          handler={RepositoryController.getRepositoryCreateView}
+        />
       </Router.Group>
     </Router.Root>
   );

new file
app/views/repository/RepositoryCreateView.tsx
@@ -0,0 +1,56 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import { Organization } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Button, Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoryCreateForm from "../../islands/RepositoryCreateForm";
+
+export interface RepositoryCreateViewProps extends CommonProps {
+  availableParentOrgs: Organization[];
+  errorMessage?: null | string;
+  initialValues?: {
+    parent_org_slug?: string;
+    repo_slug?: string;
+    repo_display_name?: string;
+    repo_short_description?: string;
+    repo_website_url?: string;
+    repo_keywords?: string[];
+  };
+}
+
+const RepositoryCreateView: ReactView<RepositoryCreateViewProps> = ({
+  availableParentOrgs,
+  commonProps,
+  errorMessage = undefined,
+  initialValues = undefined,
+}) => {
+  return (
+    <Layout {...commonProps} showSideMenu={false}>
+      <PageWrapper>
+        {errorMessage && (
+          <div className={"error_message"}>
+            <p>{errorMessage}</p>
+          </div>
+        )}
+        <form action={`/repo/new`} method={"POST"}>
+          <div data-islandid={`${RepositoryCreateForm.name}$$0`}>
+            <RepositoryCreateForm
+              availableParentOrgs={availableParentOrgs}
+              initialValues={initialValues}
+            />
+          </div>
+          {/* Submit Button */}
+          <Button type={"submit"}>Create Repository</Button>
+        </form>
+      </PageWrapper>
+    </Layout>
+  );
+};
+
+RepositoryCreateView.displayName = "RepositoryCreateView";
+export default RepositoryCreateView;

GitFOSS • v0.2.0 (#421408f) • MIT License