From 12d297d478696df9191c80bdc3722add9105f81b Mon Sep 17 00:00:00 2001
From: cuteolaf
Date: Tue, 18 Oct 2022 20:09:40 -0700
Subject: [PATCH] feat: react - project details component
---
api/public/common.css | 236 ++---
api/public/index.css | 914 ++++++++++++--------
api/public/projectDetails.css | 159 ++++
api/src/backend/routes/web/index.ts | 4 +
api/src/backend/views/projectDetails.pug | 6 +
api/src/frontend/components/option.tsx | 30 +
api/src/frontend/components/progressBar.tsx | 50 ++
api/src/frontend/components/tagsInput.tsx | 101 +++
api/src/frontend/components/textInput.tsx | 44 +
api/src/frontend/config/details-data.ts | 112 +++
api/src/frontend/login.tsx | 190 ++--
api/src/frontend/projectDetails.tsx | 251 ++++++
api/src/frontend/proposals.tsx | 185 ++--
api/src/frontend/webpack.config.js | 3 +-
14 files changed, 1635 insertions(+), 650 deletions(-)
create mode 100644 api/public/projectDetails.css
create mode 100644 api/src/backend/views/projectDetails.pug
create mode 100644 api/src/frontend/components/option.tsx
create mode 100644 api/src/frontend/components/progressBar.tsx
create mode 100644 api/src/frontend/components/tagsInput.tsx
create mode 100644 api/src/frontend/components/textInput.tsx
create mode 100644 api/src/frontend/config/details-data.ts
create mode 100644 api/src/frontend/projectDetails.tsx
diff --git a/api/public/common.css b/api/public/common.css
index 7cdbe5e8..92cf57fc 100644
--- a/api/public/common.css
+++ b/api/public/common.css
@@ -1,135 +1,141 @@
:root {
- --theme-primary: #b2ff0b;
- --theme-secondary: #411dc9;
- --theme-secondary-hover: #512dd9;
- --theme-secondary-dark: #020c67;
- --theme-secondary-dark-hover: #121c7f;
- --theme-error: #ff5a58;
- --theme-error-hover: #ff7a78;
- --theme-background: #1a1a19;
- --theme-title-color: #fff;
- --theme-ink-color: #ebeae2;
- --theme-font-family: "Aeonik", sans-serif;
-
- --theme-h1-size: 75px;
- --theme-h1-weight: normal;
-
-
-
- --theme-h2-size: 50px;
- --theme-h2-weight: normal;
-
- --dialog-title-color: var(--theme-title-color);
- --dialog-content-color: #a9a9a9; /** FIXME: really? why not theme-ink-color? */
- --dialog-scrim-background: rgba(0, 0, 0, 0.75);
- --dialog-anchor-color: var(--theme-primary);
- --dialog-strong-color: var(--dialog-title-color);
- --dialog-box-shadow:
- 0px 0px 5px #444,
- 0px 11px 15px -7px rgba(50, 50, 50, 0.2),
- 0px 24px 38px 3px rgba(50, 50, 50, 0.14),
- 0px 9px 46px 8px rgba(50, 50, 50, 0.12);
- --dialog-z-index: 1001;
-
-
- /** material design variable */
- --mdc-shape-small: 8px;
- --mdc-theme-primary: var(--theme-primary);
- --mdc-theme-secondary: var(--theme-secondary);
- --mdc-theme-secondary-hover: var(--theme-secondary-hover);
- --mdc-theme-error: var(--theme-error);
- --mdc-theme-background: var(--theme-background);
- --mdc-theme-surface: #0f0f0f;
-
- --mdc-typography-font-family: var(--theme-font-family);
-
- --mdc-text-field-fill-color: rgba(235, 234, 226, 0.11);
- --mdc-text-field-disabled-fill-color: rgba(235, 234, 226, 0.23);
- --mdc-text-field-label-ink-color: #aaa;
- --mdc-text-field-disabled-ink-color: rgba(255, 255, 255, 0.25);
- --mdc-text-field-ink-color: var(--theme-ink-color);
-
- --mdc-select-fill-color: var(--mdc-text-field-fill-color);
- --mdc-select-disabled-fill-color: var(--mdc-text-field-disabled-fill-color);
- --mdc-select-disabled-ink-color: var(--mdc-text-field-disabled-ink-color);
- --mdc-select-label-ink-color: var(--mdc-text-field-label-ink-color);
- --mdc-select-ink-color: var(--mdc-text-field-ink-color);
-
- --mdc-theme-text-primary-on-background: var(--mdc-text-field-ink-color);
- --mdc-theme-text-secondary-on-background: var(--mdc-theme-secondary);
- --mdc-theme-text-icon-on-background: var(--mdc-text-field-label-ink-color);
-
- /* --mdc-dialog-content-ink-color: var(--mdc-text-field-ink-color);
+ --theme-primary: #b2ff0b;
+ --theme-secondary: #411dc9;
+ --theme-secondary-hover: #512dd9;
+ --theme-secondary-dark: #020c67;
+ --theme-secondary-dark-hover: #121c7f;
+ --theme-error: #ff5a58;
+ --theme-error-hover: #ff7a78;
+ --theme-background: #1a1a19;
+ --theme-title-color: #fff;
+ --theme-ink-color: #ebeae2;
+ --theme-grey-text: #a6a6a6;
+ --theme-font-family: "Aeonik", sans-serif;
+ --theme-black-text: #0f0f0f;
+
+ --theme-h1-size: 75px;
+ --theme-h1-weight: normal;
+
+ --theme-h2-size: 50px;
+ --theme-h2-weight: normal;
+
+ --dialog-title-color: var(--theme-title-color);
+ --dialog-content-color: #a9a9a9; /** FIXME: really? why not theme-ink-color? */
+ --dialog-scrim-background: rgba(0, 0, 0, 0.75);
+ --dialog-anchor-color: var(--theme-primary);
+ --dialog-strong-color: var(--dialog-title-color);
+ --dialog-box-shadow: 0px 0px 5px #444,
+ 0px 11px 15px -7px rgba(50, 50, 50, 0.2),
+ 0px 24px 38px 3px rgba(50, 50, 50, 0.14),
+ 0px 9px 46px 8px rgba(50, 50, 50, 0.12);
+ --dialog-z-index: 1001;
+
+ /** material design variable */
+ --mdc-shape-small: 8px;
+ --mdc-theme-primary: var(--theme-primary);
+ --mdc-theme-secondary: var(--theme-secondary);
+ --mdc-theme-secondary-hover: var(--theme-secondary-hover);
+ --mdc-theme-error: var(--theme-error);
+ --mdc-theme-background: var(--theme-background);
+ --mdc-theme-surface: #0f0f0f;
+
+ --mdc-typography-font-family: var(--theme-font-family);
+
+ --mdc-text-field-fill-color: rgba(235, 234, 226, 0.11);
+ --mdc-text-field-disabled-fill-color: rgba(235, 234, 226, 0.23);
+ --mdc-text-field-label-ink-color: #aaa;
+ --mdc-text-field-disabled-ink-color: rgba(255, 255, 255, 0.25);
+ --mdc-text-field-ink-color: var(--theme-ink-color);
+
+ --mdc-select-fill-color: var(--mdc-text-field-fill-color);
+ --mdc-select-disabled-fill-color: var(--mdc-text-field-disabled-fill-color);
+ --mdc-select-disabled-ink-color: var(--mdc-text-field-disabled-ink-color);
+ --mdc-select-label-ink-color: var(--mdc-text-field-label-ink-color);
+ --mdc-select-ink-color: var(--mdc-text-field-ink-color);
+
+ --mdc-theme-text-primary-on-background: var(--mdc-text-field-ink-color);
+ --mdc-theme-text-secondary-on-background: var(--mdc-theme-secondary);
+ --mdc-theme-text-icon-on-background: var(--mdc-text-field-label-ink-color);
+
+ /* --mdc-dialog-content-ink-color: var(--mdc-text-field-ink-color);
--mdc-dialog-scrim-color: rgba(200, 200, 255, 0.5); */
- --button-border-radius: 3px;
- --button-padding: 20px;
- --button-font-size: 12pt;
- --primary-button-background-color: var(--theme-secondary);
- --primary-button-background-hover-color: var(--theme-primary);
- --secondary-button-background-color: #555;
- --secondary-button-background-color: black; /* rgb(5, 12, 38); */
- --secondary-button-background-hover-color: #656565;
- --secondary-button-color: white;
- --secondary-button-border: 1px solid var(--dialog-content-color);
- --disabled-button-background-color: #676767;
- --disabled-button-color: #b5b5b5;
- --base-transition: all 200ms ease-in-out;
-
-
- --hq-dialog-content-color: var(--dialog-content-color);
- --hq-dialog-strong-color: var(--dialog-strong-color);
- --hq-dialog-title-color: var(--dialog-title-color);
- --hq-dialog-anchor-color: var(--dialog-anchor-color);
- --hq-dialog-scrim-background: var(--dialog-scrim-background);
- --hq-dialog-box-shadow: var(--dialog-box-shadow);
- --hq-dialog-z-index: var(--dialog-z-index);
- --hq-dialog-font-family: var(--theme-font-family);
- --hq-button-font-size: var(--button-font-size);
- --hq-button-padding: var(--button-padding);
- --hq-button-border-radius: var(--button-border-radius);
- --hq-primary-button-background-color: var(--primary-button-background-color);
- --hq-primary-button-background-hover-color: var(--primary-button-background-hover-color);
- --hq-secondary-button-background-color: var(--secondary-button-background-color);
- --hq-secondary-button-background-hover-color: var(--secondary-button-background-hover-color);
- --hq-secondary-button-color: var(--secondary-button-color);
- --hq-secondary-button-border: var(--secondary-button-border);
- --hq-disabled-button-background-color: var(--disabled-button-background-color);
- --hq-disabled-button-color: var(--disabled-button-color);
- --hq-base-transition: var(--base-transition);
+ --button-border-radius: 3px;
+ --button-padding: 20px;
+ --button-font-size: 12pt;
+ --primary-button-background-color: var(--theme-secondary);
+ --primary-button-background-hover-color: var(--theme-primary);
+ --secondary-button-background-color: #555;
+ --secondary-button-background-color: black; /* rgb(5, 12, 38); */
+ --secondary-button-background-hover-color: #656565;
+ --secondary-button-color: white;
+ --secondary-button-border: 1px solid var(--dialog-content-color);
+ --disabled-button-background-color: #676767;
+ --disabled-button-color: #b5b5b5;
+ --base-transition: all 200ms ease-in-out;
+
+ --hq-dialog-content-color: var(--dialog-content-color);
+ --hq-dialog-strong-color: var(--dialog-strong-color);
+ --hq-dialog-title-color: var(--dialog-title-color);
+ --hq-dialog-anchor-color: var(--dialog-anchor-color);
+ --hq-dialog-scrim-background: var(--dialog-scrim-background);
+ --hq-dialog-box-shadow: var(--dialog-box-shadow);
+ --hq-dialog-z-index: var(--dialog-z-index);
+ --hq-dialog-font-family: var(--theme-font-family);
+ --hq-button-font-size: var(--button-font-size);
+ --hq-button-padding: var(--button-padding);
+ --hq-button-border-radius: var(--button-border-radius);
+ --hq-primary-button-background-color: var(--primary-button-background-color);
+ --hq-primary-button-background-hover-color: var(
+ --primary-button-background-hover-color
+ );
+ --hq-secondary-button-background-color: var(
+ --secondary-button-background-color
+ );
+ --hq-secondary-button-background-hover-color: var(
+ --secondary-button-background-hover-color
+ );
+ --hq-secondary-button-color: var(--secondary-button-color);
+ --hq-secondary-button-border: var(--secondary-button-border);
+ --hq-disabled-button-background-color: var(
+ --disabled-button-background-color
+ );
+ --hq-disabled-button-color: var(--disabled-button-color);
+ --hq-base-transition: var(--base-transition);
}
body {
- margin: 0;
- padding: 0;
-
- background-color: #000;
- background-image: url("background.png");
- background-size: cover;
- background-repeat: no-repeat;
- background-attachment: scroll;
+ margin: 0;
+ padding: 0;
+
+ background-color: #000;
+ background-image: url("background.png");
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-attachment: scroll;
}
.primary-btn {
- margin: 0;
+ margin: 0;
}
.primary-btn:disabled {
- background-color: #676767;
- color: #b5b5b5;
- cursor: unset;
+ background-color: #676767;
+ color: #b5b5b5;
+ cursor: unset;
}
.primary-btn.fullwidth {
- border-radius: 3px;
- font-weight: normal;
+ border-radius: 3px;
+ font-weight: normal;
}
@font-face {
- font-family: "Aeonik";
- src: url("AeonikTRIAL-Regular.otf") format("opentype");
+ font-family: "Aeonik";
+ src: url("AeonikTRIAL-Regular.otf") format("opentype");
}
-.mdc-dialog .mdc-dialog__title, .mdc-dialog__content {
- color: var(--mdc-text-field-ink-color) !important;
-}
\ No newline at end of file
+.mdc-dialog .mdc-dialog__title,
+.mdc-dialog__content {
+ color: var(--mdc-text-field-ink-color) !important;
+}
diff --git a/api/public/index.css b/api/public/index.css
index 59ae4e31..40832a1d 100644
--- a/api/public/index.css
+++ b/api/public/index.css
@@ -1,7 +1,7 @@
body {
- color: var(--main-text-color);
- width: 100%;
- font-family: var(--theme-font-family);
+ color: var(--main-text-color);
+ width: 100%;
+ font-family: var(--theme-font-family);
}
/*a {*/
@@ -14,265 +14,317 @@ body {
/*}*/
.hidden {
- display: none !important;
+ display: none !important;
}
h1 {
- font-size: var(--theme-h1-size);
- font-weight: var(--theme-h1-weight);
+ font-size: var(--theme-h1-size);
+ font-weight: var(--theme-h1-weight);
}
h2 {
- font-size: var(--theme-h2-size);
- font-weight: var(--theme-h2-weight);
+ font-size: var(--theme-h2-size);
+ font-weight: var(--theme-h2-weight);
}
-
li {
- list-style: none;
+ list-style: none;
}
ol {
- margin: 0;
- padding: 0;
+ margin: 0;
+ padding: 0;
+}
+
+textarea {
+ background: var(--theme-ink-color);
+ border: 1px solid black;
+ border-radius: 8px;
+ backdrop-filter: blur(8px);
+ padding: 8px;
+ width: 100%;
+ height: 100%;
+ resize: none;
+}
+
+input[type="radio"] {
+ width: 16px;
+ height: 16px;
+}
+
+.textarea-remaining {
+ margin-top: 14px;
+ text-align: right;
+ font-size: 12px;
+ line-height: 16px;
+ color: black;
}
.dialog {
- font-family: var(--hq-dialog-font-family);
+ font-family: var(--hq-dialog-font-family);
}
:not(.mdc-deprecated-list-item--disabled).mdc-deprecated-list-item {
- padding: 0;
+ padding: 0;
}
.mdc-dialog .mdc-dialog__title {
- color: var(--hq-dialog-title-color);
+ color: var(--hq-dialog-title-color);
}
.mdc-dialog .mdc-dialog__content {
- color: var(--hq-dialog-content-color);
+ color: var(--hq-dialog-content-color);
}
.mdc-dialog a {
- color: var(--hq-dialog-anchor-color);
+ color: var(--hq-dialog-anchor-color);
}
.mdc-dialog strong {
- color: var(--hq-dialog-strong-color);
+ color: var(--hq-dialog-strong-color);
}
.mdc-dialog .mdc-dialog__scrim {
- background: var(--hq-dialog-scrim-background);
+ background: var(--hq-dialog-scrim-background);
}
.mdc-dialog .mdc-dialog__surface {
- box-shadow: var(--hq-dialog-box-shadow);
+ box-shadow: var(--hq-dialog-box-shadow);
}
.mdc-dialog {
- z-index: var(--hq-dialog-z-index, 1001);
+ z-index: var(--hq-dialog-z-index, 1001);
}
-.mdc-tab .mdc-tab__text-label, .mdc-tab .mdc-tab__icon {
- color: #fff;
+.mdc-tab .mdc-tab__text-label,
+.mdc-tab .mdc-tab__icon {
+ color: #fff;
}
.tab-content a {
- color: var(--theme-primary);
+ color: var(--theme-primary);
}
#milestones li {
- color: var(--mdc-theme-text-primary-on-background);
- background-color: var(--mdc-theme-background);
- margin: 5px 0;
+ color: var(--mdc-theme-text-primary-on-background);
+ background-color: var(--mdc-theme-background);
+ margin: 5px 0;
}
#milestones .mdc-list-item__secondary-text {
- color: var(--theme-primary);
+ color: var(--theme-primary);
}
#details > h3 {
- font-weight: normal;
+ font-weight: normal;
}
#details > h3 > a {
- color: var(--theme-primary);
+ color: var(--theme-primary);
}
ul#action-list {
- list-style: none outside none;
- margin: 0;
- padding: 20px 0 0 0;
- display: flex;
- flex-direction: column;
+ list-style: none outside none;
+ margin: 0;
+ padding: 20px 0 0 0;
+ display: flex;
+ flex-direction: column;
}
ul li {
- margin: 5px 0 0 0;
- width: 100%;
- color: var(--hq-dialog-content-color);
+ margin: 5px 0 0 0;
+ width: 100%;
+ color: var(--hq-dialog-content-color);
}
.button-container {
- display: flex;
+ display: flex;
}
.button-container button {
- border-radius: var(--hq-button-border-radius, 3px);
- background-color: var(--hq-secondary-button-background-color, inherit);
- color: var(--hq-secondary-button-color);
- border: var(--hq-secondary-button-border);
- transition: var(--hq-base-transition);
- width: 100%;
- padding: var(--hq-button-padding, 10px);
- font-size: var(--hq-button-font-size);
- cursor: pointer;
+ border-radius: var(--hq-button-border-radius, 3px);
+ background-color: var(--hq-secondary-button-background-color, inherit);
+ color: var(--hq-secondary-button-color);
+ border: var(--hq-secondary-button-border);
+ transition: var(--hq-base-transition);
+ width: 100%;
+ padding: var(--hq-button-padding, 10px);
+ font-size: var(--hq-button-font-size);
+ cursor: pointer;
}
.button-container button.primary {
- background-color: var(--hq-primary-button-background-color);
- font-weight: bold;
+ background-color: var(--hq-primary-button-background-color);
+ font-weight: bold;
}
.button-container button:hover {
- background-color: var(--hq-secondary-button-background-hover-color);
+ background-color: var(--hq-secondary-button-background-hover-color);
}
.button-container button.primary:hover {
- background-color: var(--hq-primary-button-background-hover-color);
+ background-color: var(--hq-primary-button-background-hover-color);
}
.w-button {
- display: inline-block;
- padding: 9px 15px;
- background-color: #3898ec;
- color: #fff;
- border: 0;
- line-height: inherit;
- text-decoration: none;
- cursor: pointer;
- border-radius: 0;
+ display: inline-block;
+ padding: 9px 15px;
+ background-color: #3898ec;
+ color: #fff;
+ border: 0;
+ line-height: inherit;
+ text-decoration: none;
+ cursor: pointer;
+ border-radius: 0;
}
.primary-btn {
- margin-top: 16px;
- margin-right: 16px;
- padding: 12px 24px;
- -webkit-align-self: center;
- -ms-flex-item-align: center;
- -ms-grid-row-align: center;
- align-self: center;
- border: 2px solid transparent;
- background-color: #7956bf;
- -webkit-transition: color .3s,background-color .3s;
- transition: color .3s,background-color .3s;
- font-family: 'Space Grotesk',sans-serif;
- color: #fff;
- font-size: 16px;
- line-height: 22px;
- font-weight: 700;
- text-align: center;
- letter-spacing: .6px;
- text-decoration: none;
- text-transform: capitalize;
- cursor: pointer;
+ margin-top: 16px;
+ margin-right: 16px;
+ padding: 12px 24px;
+ -webkit-align-self: center;
+ -ms-flex-item-align: center;
+ -ms-grid-row-align: center;
+ align-self: center;
+ border: 2px solid transparent;
+ background-color: #7956bf;
+ -webkit-transition: color 0.3s, background-color 0.3s;
+ transition: color 0.3s, background-color 0.3s;
+ font-family: "Space Grotesk", sans-serif;
+ color: #fff;
+ font-size: 16px;
+ line-height: 22px;
+ font-weight: 700;
+ text-align: center;
+ letter-spacing: 0.6px;
+ text-decoration: none;
+ text-transform: capitalize;
+ cursor: pointer;
}
.primary-btn.in-dark {
- -webkit-align-self: center;
- -ms-flex-item-align: center;
- -ms-grid-row-align: center;
- align-self: center;
- border-radius: 46px;
- background-color: #411dc9;
- font-family: Aeoniktrial,sans-serif;
- font-weight: 400;
- text-align: right;
+ -webkit-align-self: center;
+ -ms-flex-item-align: center;
+ -ms-grid-row-align: center;
+ align-self: center;
+ border-radius: 46px;
+ background-color: #411dc9;
+ font-family: Aeoniktrial, sans-serif;
+ font-weight: 400;
+ text-align: right;
}
.primary-btn.in-dark:hover {
- background-color: #a4ff00;
- color: #000;
+ background-color: #a4ff00;
+ color: #000;
+}
+
+.secondary-btn {
+ margin-top: 16px;
+ margin-right: 16px;
+ padding: 12px 24px;
+ -webkit-align-self: center;
+ -ms-flex-item-align: center;
+ -ms-grid-row-align: center;
+ align-self: center;
+ border: 2px solid transparent;
+ background-color: white;
+ -webkit-transition: color 0.3s, background-color 0.3s;
+ transition: color 0.3s, background-color 0.3s;
+ font-family: "Space Grotesk", sans-serif;
+ color: var(--theme-secondary);
+ font-size: 16px;
+ line-height: 22px;
+ font-weight: 400;
+ text-align: center;
+ letter-spacing: 0.6px;
+ text-decoration: none;
+ text-transform: capitalize;
+ cursor: pointer;
+ border-radius: 100px;
+}
+
+.secondary-btn:hover {
+ background-color: var(--theme-primary);
+ color: #000;
}
div#layout {
- /* display: flex;
+ /* display: flex;
flex-direction: row; */
- align-content: baseline;
- height: 100%;
- width: 100vw;
- max-width: 100%;
- color: var(--theme-ink-color);
- flex-wrap: wrap;
- --hq-layout-header-height: 2.5rem;
- margin: auto;
- min-height: 100vh;
- min-height: calc(100vh - 55px);
- --hq-layout-padding: 30px;
- padding: 0;
- width: 100%;
- max-width: var(--hq-layout-max-screen);
- /* overflow: hidden; */
- --hq-layout-max-screen: 1366px;
+ align-content: baseline;
+ height: 100%;
+ width: 100vw;
+ max-width: 100%;
+ color: var(--theme-ink-color);
+ flex-wrap: wrap;
+ --hq-layout-header-height: 2.5rem;
+ margin: auto;
+ min-height: 100vh;
+ min-height: calc(100vh - 55px);
+ --hq-layout-padding: 30px;
+ padding: 0;
+ width: 100%;
+ max-width: var(--hq-layout-max-screen);
+ /* overflow: hidden; */
+ --hq-layout-max-screen: 1366px;
}
.padded {
- transition: padding 200ms ease-in-out;
- padding: var(--hq-layout-padding);
+ transition: padding 200ms ease-in-out;
+ padding: var(--hq-layout-padding);
}
#header-wrapper {
- width: 100%;
- position: relative;
- top: 0;
- background: rgba(0,0,0,0.0);
- transition:
- background 200ms ease-in-out,
- box-shadow 200ms ease-in-out,
- padding 200ms ease-in-out;
+ width: 100%;
+ position: relative;
+ top: 0;
+ background: rgba(0, 0, 0, 0);
+ transition: background 200ms ease-in-out, box-shadow 200ms ease-in-out,
+ padding 200ms ease-in-out;
}
#header-wrapper.hidden {
- /* box-shadow: 0 0 5px #aaa; */
- z-index: 1;
- position: sticky;
- transform: translateY(-125px);
+ /* box-shadow: 0 0 5px #aaa; */
+ z-index: 1;
+ position: sticky;
+ transform: translateY(-125px);
}
#header-wrapper.hidden-transition {
- transition: all 150ms ease-in-out;
+ transition: all 150ms ease-in-out;
}
#header-wrapper.sticky {
- box-shadow: 0 0 5px #aaa;
- z-index: 1;
- position: sticky;
- background: rgba(0,0,0,.9);
- transform: translateY(0px);
- transition: transform 150ms ease-in-out;
+ box-shadow: 0 0 5px #aaa;
+ z-index: 1;
+ position: sticky;
+ background: rgba(0, 0, 0, 0.9);
+ transform: translateY(0px);
+ transition: transform 150ms ease-in-out;
}
#footer {
- width: 100%;
- position: fixed;
- bottom: 0;
- transition: all 150ms ease-in-out;
+ width: 100%;
+ position: fixed;
+ bottom: 0;
+ transition: all 150ms ease-in-out;
}
#footer.hidden {
- box-shadow: 0 0 5px #aaa;
- transform: translateY(75px);
+ box-shadow: 0 0 5px #aaa;
+ transform: translateY(75px);
}
#footer.hidden-transition {
- transition: all 150ms ease-in-out;
+ transition: all 150ms ease-in-out;
}
#footer.sticky {
- box-shadow: 0 0 5px #aaa;
- transform: translateY(0px);
- transition: transform 150ms ease-in-out;
+ box-shadow: 0 0 5px #aaa;
+ transform: translateY(0px);
+ transition: transform 150ms ease-in-out;
}
#main-header {
- /* background: #ccc;
+ /* background: #ccc;
background: #fff; */
- display: flex;
- z-index: 1;
- /*height: var(--hq-layout-header-height);*/
- flex-wrap: wrap;
- line-height: 1.15rem;
+ display: flex;
+ z-index: 1;
+ /*height: var(--hq-layout-header-height);*/
+ flex-wrap: wrap;
+ line-height: 1.15rem;
}
/* #main-header a,
#main-header a:hover {
@@ -280,135 +332,130 @@ div#layout {
color: var(--main-text-color);
} */
#main-header h1 {
- font-size: 1.15rem;
- margin: 0;
- /* padding: 6px 0 0 0; */
+ font-size: 1.15rem;
+ margin: 0;
+ /* padding: 6px 0 0 0; */
}
.spacer {
- flex-grow: 1;
- display: block;
- /*width: 20%;*/
+ flex-grow: 1;
+ display: block;
+ /*width: 20%;*/
}
.main-title {
- display: block;
- margin: 0 0 0 .5rem;
- margin: 0;
- width: 160px;
- height: 43px;
+ display: block;
+ margin: 0 0 0 0.5rem;
+ margin: 0;
+ width: 160px;
+ height: 43px;
}
#main-content {
- overflow: auto;
- width: 100vw;
- min-height: inherit;
+ overflow: auto;
+ width: 100vw;
+ min-height: inherit;
}
.drawer {
- position: fixed;
- top: 0;
- height: 100vh;
- width: 80vw;
- overflow: auto;
- transition: transform 200ms cubic-bezier(1,0,0,1);
- transition: all 200ms ease-in-out;
-}
-#left-drawer.open, #right-drawer.open {
- transform: translateX(0);
- z-index: 2;
+ position: fixed;
+ top: 0;
+ height: 100vh;
+ width: 80vw;
+ overflow: auto;
+ transition: transform 200ms cubic-bezier(1, 0, 0, 1);
+ transition: all 200ms ease-in-out;
+}
+#left-drawer.open,
+#right-drawer.open {
+ transform: translateX(0);
+ z-index: 2;
}
#left-drawer {
- left: 0;
- background: rgba(90,90,90,.90);
- transform: translateX(-100%);
- display: flex;
- flex-wrap: wrap;
- z-index: 1;
+ left: 0;
+ background: rgba(90, 90, 90, 0.9);
+ transform: translateX(-100%);
+ display: flex;
+ flex-wrap: wrap;
+ z-index: 1;
}
#right-drawer {
- right: 0;
- /* background: rgba(2,12,103,0.1); */
- /* background: rgba(255,255,255,0.1); */
- background: #000;
- transform: translateX(100%);
- z-index: 1;
+ right: 0;
+ /* background: rgba(2,12,103,0.1); */
+ /* background: rgba(255,255,255,0.1); */
+ background: #000;
+ transform: translateX(100%);
+ z-index: 1;
}
.modal {
- /*height: 100vh;*/
- width: 100vw;
- max-width: 100%;
- transform: translateX(-100vw);
- position: fixed;
- overflow: hidden;
- z-index: 1;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- /* background: rgba(90,90,0,.25); */
- background: rgba(200,200,200,.10);
- opacity: 0;
+ /*height: 100vh;*/
+ width: 100vw;
+ max-width: 100%;
+ transform: translateX(-100vw);
+ position: fixed;
+ overflow: hidden;
+ z-index: 1;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ /* background: rgba(90,90,0,.25); */
+ background: rgba(200, 200, 200, 0.1);
+ opacity: 0;
}
.modal.show {
- transform: translateX(0);
- opacity: 1;
+ transform: translateX(0);
+ opacity: 1;
}
#loading-modal {
- display: flex;
- justify-content: center;
- transition: opacity 1000ms ease-in;
+ display: flex;
+ justify-content: center;
+ transition: opacity 1000ms ease-in;
}
#loading-modal svg {
- max-width: 10em;
+ max-width: 10em;
}
#context-menu {
- display: flex;
- /*justify-content: space-between;*/
- justify-content: flex-end;
- width: 55%;
- max-width: 200px;
- height: 35px;
+ display: flex;
+ /*justify-content: space-between;*/
+ justify-content: flex-end;
+ width: 55%;
+ max-width: 200px;
+ height: 35px;
}
#context-menu.hidden {
- display: none;
+ display: none;
}
.context-menu-spacer {
- border-right: 1px dotted #ccc;
- margin: 0 5px;
+ border-right: 1px dotted #ccc;
+ margin: 0 5px;
}
-
-
-@media screen
- /* and (min-width: 500px) */
-and (max-width: 500px) {
- .padded {
- padding: calc(var(--hq-layout-padding) / 2);
- }
+@media screen /* and (min-width: 500px) */ and (max-width: 500px) {
+ .padded {
+ padding: calc(var(--hq-layout-padding) / 2);
+ }
}
/**
* Left hand side menu is now fixed in place and hamburger goes away.
*/
-@media screen
-and (min-width: 500px) {
-
- :host {
- /* min-width: 700px; */
- max-width: var(--hq-layout-max-screen);
- }
- #main-content {
- display: flex;
- align-items: stretch;
- }
- slot[name="main"] {
- width: 1%;
- display: block;
- flex-grow: 1;
- /* padding: 0 10px; */
- }
- /**
+@media screen and (min-width: 500px) {
+ :host {
+ /* min-width: 700px; */
+ max-width: var(--hq-layout-max-screen);
+ }
+ #main-content {
+ display: flex;
+ align-items: stretch;
+ }
+ slot[name="main"] {
+ width: 1%;
+ display: block;
+ flex-grow: 1;
+ /* padding: 0 10px; */
+ }
+ /**
* FIXME: if we ever want to use the left-drawer, we'll need to uncomment
* the following:
*/
- /* #left-drawer {
+ /* #left-drawer {
width: auto;
transform: none;
@@ -429,85 +476,84 @@ and (min-width: 500px) {
}
@media screen and (min-width: 1366px) {
- #header-wrapper.sticky {
- box-shadow: none;
- z-index: auto;
- position: initial;
- background: inherit;
- transform: translateY(0px);
- transition: none;
- }
+ #header-wrapper.sticky {
+ box-shadow: none;
+ z-index: auto;
+ position: initial;
+ background: inherit;
+ transform: translateY(0px);
+ transition: none;
+ }
}
@media screen and (min-width: 700px) {
- .drawer {
- /* width: 555px; */
- width: 30vw;
- min-width: 300px;
- max-width: 555px;
- }
+ .drawer {
+ /* width: 555px; */
+ width: 30vw;
+ min-width: 300px;
+ max-width: 555px;
+ }
}
-
.context-menu {
- display: flex;
- /* justify-content: space-between; */
- justify-content: end;
- /* min-width: 55%; */
- /* height: 35px; */
+ display: flex;
+ /* justify-content: space-between; */
+ justify-content: end;
+ /* min-width: 55%; */
+ /* height: 35px; */
}
.context-menu-item {
- /* nothing yet */
+ /* nothing yet */
}
.context-menu-item > a {
- display: block;
- font-size: 25pt;
- padding: 5px;
- padding-left: 6px;
- border-radius: 50%;
- box-shadow: 0 0 4px #55a;
- color: var(--theme-ink-color);
+ display: block;
+ font-size: 25pt;
+ padding: 5px;
+ padding-left: 6px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #55a;
+ color: var(--theme-ink-color);
}
.context-menu-item > a:hover {
- box-shadow: 0 0 10px #55a;
+ box-shadow: 0 0 10px #55a;
}
#main-menu {
- cursor: pointer;
+ cursor: pointer;
}
/* #pages > * {
padding: 0;
} */
#not-found {
- margin: 0;
- padding: 0;
+ margin: 0;
+ padding: 0;
}
#main {
- /* margin-bottom: 100px; */
- margin-top: 5px;
+ /* margin-bottom: 100px; */
+ margin-top: 5px;
}
#breadcrumbs {
- list-style: none;
- margin: 0;
- padding: 0 var(--main-side-margin) 0 5px;
- display: block;
- overflow: auto;
- white-space: nowrap;
-
- /* font-weight: bold;
+ list-style: none;
+ margin: 0;
+ padding: 0 var(--main-side-margin) 0 5px;
+ display: block;
+ overflow: auto;
+ white-space: nowrap;
+
+ /* font-weight: bold;
font-size: 1.25rem; */
}
#breadcrumbs li {
- margin: 0 6px 0 0;
- padding: 10px 0;
- margin: 0;
- display: none;
+ margin: 0 6px 0 0;
+ padding: 10px 0;
+ margin: 0;
+ display: none;
}
#breadcrumbs li.show {
- display: inline-block;
+ display: inline-block;
}
/*#breadcrumbs li:before {
content: "• ";
@@ -518,124 +564,276 @@ and (min-width: 500px) {
content: "";
}*/
#breadcrumbs li i {
- /* vertical-align: middle; */
- /* padding: 0 0 3px 0; */
- margin: 0 1px;
- vertical-align: bottom;
+ /* vertical-align: middle; */
+ /* padding: 0 0 3px 0; */
+ margin: 0 1px;
+ vertical-align: bottom;
}
#breadcrumbs li a {
- text-decoration: none;
- color: inherit;
- margin-bottom: 2px;
+ text-decoration: none;
+ color: inherit;
+ margin-bottom: 2px;
}
[slot] paper-button {
- margin: 0;
- text-decoration: none;
- color: var(--main-text-color);
- padding: 0 5px 0 0;
- min-width: 0px;
- border-radius: 50%;
- background: #ffffff55;
- height: 32px;
- border: 1px solid white;
- box-shadow: 0 0 5px #aaa;
- box-shadow: 0 0 3px #ddd;
- box-shadow: none;
+ margin: 0;
+ text-decoration: none;
+ color: var(--main-text-color);
+ padding: 0 5px 0 0;
+ min-width: 0px;
+ border-radius: 50%;
+ background: #ffffff55;
+ height: 32px;
+ border: 1px solid white;
+ box-shadow: 0 0 5px #aaa;
+ box-shadow: 0 0 3px #ddd;
+ box-shadow: none;
}
[slot] paper-button i {
- font-size: var(--icon-size);
+ font-size: var(--icon-size);
}
.context-menu-spacer {
- border-right: 1px dotted #fff;
- margin: 0 5px;
+ border-right: 1px dotted #fff;
+ margin: 0 5px;
}
#hamburger {
- /* display: none; */
+ /* display: none; */
+}
+#hamburger.show {
+ display: initial;
}
- #hamburger.show {
- display: initial;
- }
-
#breadcrumbs li.show {
- padding: 0;
- /* margin-top: -20px; */
- font-weight: lighter;
+ padding: 0;
+ /* margin-top: -20px; */
+ font-weight: lighter;
}
#breadcrumbs li.show:last-child a {
- /* font-size: 1.5em; */
- font-weight: bold;
+ /* font-size: 1.5em; */
+ font-weight: bold;
}
#breadcrumbs li.show a {
- font-size: .9em;
+ font-size: 0.9em;
}
#layout {
- min-height: unset;
+ min-height: unset;
}
@media screen and (min-width: 700px) {
- #breadcrumbs {
- padding: 0;
- }
+ #breadcrumbs {
+ padding: 0;
+ }
}
@keyframes rotation {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(359deg);
- }
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(359deg);
+ }
}
-
-
-
#right-drawer {
- flex-grow: 1;
- display: block;
+ flex-grow: 1;
+ display: block;
}
nav {
- color: var(--theme-ink-color);
+ color: var(--theme-ink-color);
}
nav > ul {
- list-style: none;
- margin: 0;
- padding: 28px 0;
- font-size: 1.25rem;
- font-family: var(--theme-font-family);
+ list-style: none;
+ margin: 0;
+ padding: 28px 0;
+ font-size: 1.25rem;
+ font-family: var(--theme-font-family);
}
nav > ul > li {
- transition: background 200ms ease-in-out;
- background: #000;
- flex-grow: 1;
+ transition: background 200ms ease-in-out;
+ background: #000;
+ flex-grow: 1;
}
nav > ul > li:hover {
- background: var(--theme-secondary-dark);
- transition: background 200ms ease-in-out;
+ background: var(--theme-secondary-dark);
+ transition: background 200ms ease-in-out;
}
-nav > ul .selected, nav > ul .selected:hover {
- font-weight: bold;
- background-color: var(--theme-secondary-dark-hover); /* ??? */
- background: var(--theme-secondary-dark-hover);
+nav > ul .selected,
+nav > ul .selected:hover {
+ font-weight: bold;
+ background-color: var(--theme-secondary-dark-hover); /* ??? */
+ background: var(--theme-secondary-dark-hover);
}
nav > ul .selected i ~ span::after {
- /*content: "*";*/
+ /*content: "*";*/
}
nav a {
- color: white;
- display: flex;
- text-decoration: none;
- padding: 12px 20px;
+ color: white;
+ display: flex;
+ text-decoration: none;
+ padding: 12px 20px;
}
.drawer span {
- margin-left: 10px;
+ margin-left: 10px;
}
nav i.material-icons {
- font-size: 24px;
- padding-top: 2px;
+ font-size: 24px;
+ padding-top: 2px;
+}
+
+.progressbar-container {
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+}
+
+.progress-step-circle {
+ width: 16px;
+ height: 16px;
+ border-radius: 100%;
+}
+
+.progress-step-circle.active {
+ background-color: var(--theme-primary);
+}
+
+.progress-step-circle.disabled {
+ background-color: var(--theme-grey-text);
+}
+
+.progress-step-text {
+ color: var(--theme-grey-text);
+ font-size: 12px;
+ line-height: 1;
+ position: absolute;
+ padding-top: 12px;
+}
+
+.progress-step-text.center {
+ transform: translateX(calc(-50% + 8px));
+}
+
+.progress-step-text.right {
+ transform: translateX(calc(-100% + 16px));
+}
+
+.progress-bar-back {
+ position: absolute;
+ height: 6px;
+ background: var(--theme-grey-text);
+ left: 4px;
+ right: 4px;
+ top: 6px;
+ bottom: 6px;
+}
+
+.progress-bar-progress {
+ position: absolute;
+ height: 6px;
+ background: var(--theme-primary);
+ left: 4px;
+ right: 4px;
+ top: 6px;
+ bottom: 6px;
+ z-index: 10;
+}
+
+.option-container {
+ display: flex;
+ flex-direction: column;
+ user-select: none;
+}
+
+.option-inner {
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ align-items: center;
+}
+
+.field-name {
+ font-size: 20px;
+ line-height: 1.15;
+ color: var(--theme-black-text);
+ font-weight: bold;
+ margin: 0px;
+}
+
+.option-children-container {
+ display: flex;
+ flex-direction: column;
+ padding-top: 4px;
+ padding-left: 32px;
+}
+
+.new-tag-input {
+ border: 1px solid var(--theme-grey-text);
+ padding: 10px;
+ border-radius: 20px;
+ background: white;
+}
+
+.selected-tags {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 4px;
+ background: var(--theme-ink-color);
+ border: 1px solid var(--theme-black-text);
+ border-radius: 20px;
+ backdrop-filter: blur(8px);
+ padding: 8px 8px 8px 16px;
+}
+
+.selected-tag-item {
+ padding: 6px 12px;
+ color: var(--theme-secondary);
+ border: 1px solid var(--theme-black-text);
+ background: var(--theme-ink-color);
+ display: flex;
+ align-items: center;
+ border-radius: 1000px;
+ text-transform: capitalize;
+ user-select: none;
+}
+
+.unselect-tag {
+ margin: 4px;
+ text-transform: none;
+ color: var(--theme-secondary);
+ cursor: pointer;
+}
+
+.tags-suggestion-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding-top: 32px;
+}
+
+.tag-suggestion {
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+ align-items: center;
+ justify-content: center;
+ background-color: white;
+ border-radius: 1000px;
+ color: var(--theme-secondary);
+ padding: 8px 12px;
+}
+
+.tag-suggestion-text {
+ user-select: none;
+}
+
+.tag-suggest-button {
+ cursor: pointer;
+}
+
+.tag-suggest-button:active {
+ transform: scale(1.5);
}
diff --git a/api/public/projectDetails.css b/api/public/projectDetails.css
new file mode 100644
index 00000000..9f166c5e
--- /dev/null
+++ b/api/public/projectDetails.css
@@ -0,0 +1,159 @@
+h1 {
+ font-size: 52px;
+ line-height: 1.1;
+ color: var(--theme-title-color);
+ margin: 0px;
+ padding: 16px 0px;
+}
+
+p {
+ font-size: 20px;
+ line-height: 1.15;
+}
+
+.field-input {
+ background-color: var(--theme-ink-color);
+ /* opacity: 0.11; */
+ border: solid 1px black;
+ border-radius: 8px;
+ backdrop-filter: blur(8px);
+ padding: 8px 8px 8px 16px;
+ width: 100%;
+ font-size: 16px;
+ line-height: 1.5;
+}
+
+.project-details-container {
+ display: flex;
+ flex-direction: row;
+ width: 90vw;
+ min-height: 650px;
+ max-height: 650px;
+ max-width: 1250px;
+ /* margin-left: auto; */
+ margin-right: auto;
+ border-radius: 20px;
+}
+
+.left-panel {
+ background-color: #2c2c2c;
+ padding: 64px 32px 64px 48px;
+ width: 50%;
+}
+
+.right-panel {
+ display: flex;
+ flex-direction: column;
+ background-color: var(--theme-ink-color);
+ padding: 32px 64px;
+ width: 50%;
+}
+
+.heading {
+ padding: 112px 32px 64px 0px;
+}
+
+.help {
+ color: var(--theme-ink-color);
+ padding-right: 48px;
+ padding-bottom: 8px;
+}
+
+.contents {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: flex-end;
+ padding-bottom: 16px;
+}
+
+.buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+}
+
+.name-panel-input-wrapper {
+ margin-top: 24px;
+ margin-bottom: 40px;
+}
+
+.name-panel-name-examples {
+ display: flex;
+ flex-direction: column;
+ margin-top: 16px;
+ padding-bottom: 128px;
+}
+
+.name-panel-name-example {
+ font-size: 20px;
+ color: black;
+ line-height: 1.5;
+ margin: 0px;
+}
+
+.description-panel {
+ display: flex;
+ flex-direction: column;
+ height: 90%;
+}
+
+.description-container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ margin-top: 24px;
+}
+
+.scope-container {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ padding-bottom: 80px;
+}
+
+.scope-item-description {
+ color: var(--theme-black-text);
+ font-size: 16px;
+ line-height: 1.25;
+}
+
+.time-container {
+ display: flex;
+ flex-direction: column;
+ gap: 64px;
+ margin-bottom: 32px;
+}
+
+.budget-input-container {
+ position: relative;
+ margin-top: 24px;
+ margin-bottom: 32px;
+}
+
+.budget-currency-container {
+ position: absolute;
+ top: 0px;
+ left: 8px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ color: var(--theme-black-text);
+}
+
+.budget-description {
+ margin-bottom: 160px;
+ padding-right: 112px;
+}
+
+.industry-container {
+ padding-top: 24px;
+ padding-bottom: 80px;
+}
+
+.skills-container {
+ padding-top: 24px;
+ padding-bottom: 80px;
+}
diff --git a/api/src/backend/routes/web/index.ts b/api/src/backend/routes/web/index.ts
index cd3a5dc4..0317f536 100644
--- a/api/src/backend/routes/web/index.ts
+++ b/api/src/backend/routes/web/index.ts
@@ -28,6 +28,10 @@ router.get("/projects/:projectId", (req, res) => {
res.render("details")
});
+router.get("/project-details", (req, res) => {
+ res.render("projectDetails");
+});
+
router.use((_req, res, next) => {
res.render("legacy")
});
diff --git a/api/src/backend/views/projectDetails.pug b/api/src/backend/views/projectDetails.pug
new file mode 100644
index 00000000..05f3ff85
--- /dev/null
+++ b/api/src/backend/views/projectDetails.pug
@@ -0,0 +1,6 @@
+extends layout.pug
+block content
+ link(rel='stylesheet' href='/public/projectDetails.css')
+ h1 Submit Project Details
+ div(id="project-details" name="details")
+ script(src="/public/lib/projectDetails.js" type="text/javascript")
diff --git a/api/src/frontend/components/option.tsx b/api/src/frontend/components/option.tsx
new file mode 100644
index 00000000..0e6697ec
--- /dev/null
+++ b/api/src/frontend/components/option.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+
+export type OptionProps = {
+ label: string;
+ value: string;
+ checked?: boolean;
+ children?: React.ReactNode;
+ onSelect: () => void;
+};
+
+export class Option extends React.Component {
+ render() {
+ return (
+
+
+
{
+ e.target.checked && this.props.onSelect();
+ }}
+ />
+
{this.props.label}
+
+
{this.props.children}
+
+ );
+ }
+}
diff --git a/api/src/frontend/components/progressBar.tsx b/api/src/frontend/components/progressBar.tsx
new file mode 100644
index 00000000..27b950ad
--- /dev/null
+++ b/api/src/frontend/components/progressBar.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+
+export type ProgressBarProps = {
+ titleArray: Array;
+ currentValue: number;
+};
+
+export type ProgressBarState = {};
+
+export class ProgressBar extends React.Component<
+ ProgressBarProps,
+ ProgressBarState
+> {
+ render() {
+ const { titleArray, currentValue } = this.props;
+ return (
+
+ {titleArray?.map((title, index) => (
+
+
= index ? "active" : "disabled"
+ }`}
+ >
+
0 && index < titleArray.length - 1
+ ? "center"
+ : index === titleArray.length - 1
+ ? "right"
+ : ""
+ }`}
+ >
+ {title}
+
+
+ ))}
+
+
+
+ );
+ }
+}
diff --git a/api/src/frontend/components/tagsInput.tsx b/api/src/frontend/components/tagsInput.tsx
new file mode 100644
index 00000000..b0e13407
--- /dev/null
+++ b/api/src/frontend/components/tagsInput.tsx
@@ -0,0 +1,101 @@
+import React, { KeyboardEvent } from "react";
+
+export type TagsInputProps = {
+ tags: string[];
+ suggestData: string[];
+ onChange: (tags: string[]) => void;
+};
+
+export type TagsInputState = {
+ tags: string[];
+ input: string;
+};
+
+export class TagsInput extends React.Component {
+ constructor(props: TagsInputProps) {
+ super(props);
+ this.state = {
+ tags: props.tags,
+ input: "",
+ };
+ }
+
+ handleDelete = (targetIndex: number) => {
+ this.setState(
+ {
+ ...this.state,
+ tags: this.state.tags.filter((tag, index) => index !== targetIndex),
+ },
+ () => this.props.onChange(this.state.tags)
+ );
+ };
+
+ handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Tab") {
+ e.preventDefault();
+ }
+ if (["Tab", "Enter"].includes(e.key) && this.state.input) {
+ this.setState(
+ {
+ input: "",
+ tags: [...this.state.tags, this.state.input],
+ },
+ () => this.props.onChange(this.state.tags)
+ );
+ }
+ };
+
+ addItem = (item: string) => {
+ this.setState(
+ {
+ ...this.state,
+ tags: [...this.state.tags, item],
+ },
+ () => this.props.onChange(this.state.tags)
+ );
+ };
+
+ render() {
+ return (
+ <>
+
+ {this.props.tags.map((tag, i) => (
+
+ {tag}
+
this.handleDelete(i)}
+ >
+ x
+
+
+ ))}
+
+ this.setState({ ...this.state, input: e.target.value })
+ }
+ onKeyDown={this.handleKeyDown}
+ />
+
+
+ {this.props.suggestData
+ .filter((item: string) => this.state.tags.indexOf(item) === -1)
+ .map((item, index) => (
+
+ {item}
+ this.addItem(item)}
+ >
+ +
+
+
+ ))}
+
+ >
+ );
+ }
+}
diff --git a/api/src/frontend/components/textInput.tsx b/api/src/frontend/components/textInput.tsx
new file mode 100644
index 00000000..9e079edb
--- /dev/null
+++ b/api/src/frontend/components/textInput.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+
+export type TextInputState = {
+ remaining: number;
+};
+
+export interface TextInputProps
+ extends React.DetailedHTMLProps<
+ React.TextareaHTMLAttributes,
+ HTMLTextAreaElement
+ > {
+ maxLength?: number;
+ onChange: (e: React.ChangeEvent) => void;
+}
+
+export class TextInput extends React.Component {
+ constructor(props: TextInputProps) {
+ super(props);
+ this.state = {
+ remaining: props.maxLength ?? 0,
+ };
+ }
+ handleChange = (e: React.ChangeEvent) => {
+ this.props.maxLength &&
+ this.setState({
+ ...this.state,
+ remaining: this.props.maxLength - e.target.value.length,
+ });
+ this.props.onChange(e);
+ };
+
+ render() {
+ return (
+ <>
+
+ {this.props.maxLength && (
+ {`${
+ this.state.remaining
+ } character${this.state.remaining !== 1 ? "s" : ""} remaining`}
+ )}
+ >
+ );
+ }
+}
diff --git a/api/src/frontend/config/details-data.ts b/api/src/frontend/config/details-data.ts
new file mode 100644
index 00000000..5d9ea876
--- /dev/null
+++ b/api/src/frontend/config/details-data.ts
@@ -0,0 +1,112 @@
+export const stepData = [
+ {
+ heading: "First, let's start with a clear name",
+ content: `This helps your brief post stand out to the right candidates.
+ It’s the first thing they’ll see, so make it count!`,
+ progress: 0,
+ },
+ {
+ heading: "Great! What industry does your project fall under?",
+ content: `This helps your brief post stand out to the right candidates.
+ It’s the first thing they’ll see, so make it count!`,
+ progress: 0.5,
+ },
+ {
+ heading: "Describe the project you are envisioning.",
+ content: `This is how imbuers figure out what you need and why you’re great to work with!
+ Include your expectations about the task or deliverable, what you’re looking for in a work relationship, and anything unique about your project, team, or company.`,
+ next: "Skills",
+ progress: 0.5,
+ },
+ {
+ heading: "What skills do you require?",
+ content: `This helps your brief post stand out to the right candidates.
+ It’s the first thing they’ll see, so make it count!`,
+ next: "Scope",
+ progress: 1.0,
+ },
+ {
+ heading: "Next, estimate the scope of your project.",
+ content: `Consider the size of your project, the team you will require and the time it will take.`,
+ next: "Time",
+ progress: 2.0,
+ },
+ {
+ heading: "How long do you estimate your project will take?",
+ content: `This is not set in stone, but it will offer those that submit guidance on what you are expecting.`,
+ next: "Budget",
+ progress: 2.5,
+ },
+ {
+ heading: "Almost done! Tell us about the budget you have in mind.",
+ content:
+ "This will help people submit proposals that are within your range.",
+ progress: 3.0,
+ },
+];
+
+export const nameExamples = ["Lorem Ipsum", "Lorem Ipsum", "Lorem Ipsum"];
+
+export const scopeData = [
+ {
+ label: "Complex",
+ value: "complex",
+ description:
+ "A long term project with complex initiatives, intersecting tasks and numeroous teams.",
+ },
+ {
+ label: "Large",
+ value: "large",
+ description:
+ "A long term project with multiple tasks and requirements, with well defined milestones and a set plan",
+ },
+ {
+ label: "Medium",
+ value: "medium",
+ description: "A well-defined project, with tasks already mapped out",
+ },
+ {
+ label: "Small",
+ value: "small",
+ description: "A relatively fast and straightforward project",
+ },
+];
+
+export const timeData = [
+ {
+ label: "More than a year",
+ value: "1",
+ },
+ {
+ label: "More than 6 months",
+ value: "2",
+ },
+ {
+ label: "3-6 months",
+ value: "3",
+ },
+ {
+ label: "1 to 3 months",
+ value: "4",
+ },
+];
+
+export const industryData = [
+ "Web3",
+ "DeFi",
+ "Education",
+ "Agriculture",
+ "Communications",
+ "Health",
+ "Wellness",
+ "Energy",
+ "Sustainability",
+ "Arts and Culture",
+ "Entertainment",
+ "Real Estate",
+ "Technology",
+ "Supply Chain",
+];
+
+
+export const skillData = ['Substrate', 'Rust', 'Polkadot', 'Kusama', 'React', 'Typescript'];
\ No newline at end of file
diff --git a/api/src/frontend/login.tsx b/api/src/frontend/login.tsx
index 0739a2bd..e2ab6818 100644
--- a/api/src/frontend/login.tsx
+++ b/api/src/frontend/login.tsx
@@ -1,118 +1,132 @@
-import * as React from 'react';
-import * as ReactDOMClient from 'react-dom/client';
+import * as React from "react";
+import * as ReactDOMClient from "react-dom/client";
-import {Dialogue} from './components/dialogue';
-import {AccountChoice} from './components/accountChoice';
+import { Dialogue } from "./components/dialogue";
+import { AccountChoice } from "./components/accountChoice";
-import {signWeb3Challenge} from "./utils/polkadot";
-import {InjectedAccountWithMeta} from "@polkadot/extension-inject/types";
+import { signWeb3Challenge } from "./utils/polkadot";
+import { InjectedAccountWithMeta } from "@polkadot/extension-inject/types";
import { SignerResult } from "@polkadot/api/types";
const getAPIHeaders = {
- "accept": "application/json",
+ accept: "application/json",
};
const postAPIHeaders = {
- ...getAPIHeaders,
- "content-type": "application/json",
+ ...getAPIHeaders,
+ "content-type": "application/json",
};
-type LoginProps = {}
+type LoginProps = {};
type LoginState = {
- showPolkadotAccounts: boolean
-}
+ showPolkadotAccounts: boolean;
+};
-async function getAccountAndSign (account: InjectedAccountWithMeta) {
- const resp = await fetch(
- `/auth/web3/${account.meta.source}/`,
- {
- headers: postAPIHeaders,
- method: "post",
- body: JSON.stringify(account)
- }
- );
+async function getAccountAndSign(account: InjectedAccountWithMeta) {
+ const resp = await fetch(`/auth/web3/${account.meta.source}/`, {
+ headers: postAPIHeaders,
+ method: "post",
+ body: JSON.stringify(account),
+ });
- if (resp.ok) {
- // could be 200 or 201
- const { user, web3Account } = await resp.json();
- const signature = await signWeb3Challenge(
- account, web3Account.challenge
- );
+ if (resp.ok) {
+ // could be 200 or 201
+ const { user, web3Account } = await resp.json();
+ const signature = await signWeb3Challenge(account, web3Account.challenge);
- if (signature) {
- return {signature, user};
- } else {
- // TODO: UX for no way to sign challenge?
- }
+ if (signature) {
+ return { signature, user };
+ } else {
+ // TODO: UX for no way to sign challenge?
}
+ }
}
-async function authorise(signature: SignerResult, account: InjectedAccountWithMeta) {
- const resp = await fetch(
- `/auth/web3/${account.meta.source}/callback`,
- {
- headers: postAPIHeaders,
- method: "post",
- body: JSON.stringify({
- signature: signature.signature,
- address: account.address,
- })
- }
- );
+async function authorise(
+ signature: SignerResult,
+ account: InjectedAccountWithMeta
+) {
+ const resp = await fetch(`/auth/web3/${account.meta.source}/callback`, {
+ headers: postAPIHeaders,
+ method: "post",
+ body: JSON.stringify({
+ signature: signature.signature,
+ address: account.address,
+ }),
+ });
- if (resp.ok) {
- const redirect = new URLSearchParams(window.location.search).get("redirect") || "/dapp"
- const isRelative = new URL(document.baseURI).origin === new URL(redirect, document.baseURI).origin;
- if (isRelative) {
- location.replace(redirect);
- }
- else {
- location.replace("/dapp");
- }
+ if (resp.ok) {
+ const redirect =
+ new URLSearchParams(window.location.search).get("redirect") || "/dapp";
+ const isRelative =
+ new URL(document.baseURI).origin ===
+ new URL(redirect, document.baseURI).origin;
+ if (isRelative) {
+ location.replace(redirect);
} else {
- // TODO: UX for 401
+ location.replace("/dapp");
}
+ } else {
+ // TODO: UX for 401
+ }
}
class Login extends React.Component {
- state: LoginState = {
- showPolkadotAccounts: false
- }
+ state: LoginState = {
+ showPolkadotAccounts: false,
+ };
- async clicked() {
- this.setState({showPolkadotAccounts: true});
- }
+ async clicked() {
+ this.setState({ showPolkadotAccounts: true });
+ }
- async accountSelected(account: InjectedAccountWithMeta) {
- const result = await getAccountAndSign(account);
- await authorise(result?.signature as SignerResult, account)
- }
+ async accountSelected(account: InjectedAccountWithMeta) {
+ const result = await getAccountAndSign(account);
+ await authorise(result?.signature as SignerResult, account);
+ }
- render() {
- if (this.state.showPolkadotAccounts) {
- return (
- this.accountSelected(account)} />
- );
- } else {
- return (
- Please use the link below to sign in.
- } actionList={
-
-
-
-
- this.clicked()}
- className="mdc-deprecated-list-item__text">{"Sign in with your polkadot{.js} extension"}
-
- }/>
- );
- }
+ render() {
+ if (this.state.showPolkadotAccounts) {
+ return (
+
+ this.accountSelected(account)
+ }
+ />
+ );
+ } else {
+ return (
+ Please use the link below to sign in.}
+ actionList={
+
+
+
+
+ this.clicked()}
+ className="mdc-deprecated-list-item__text"
+ >
+ {"Sign in with your polkadot{.js} extension"}
+
+
+ }
+ />
+ );
}
+ }
}
document.addEventListener("DOMContentLoaded", function (event) {
- ReactDOMClient.createRoot(document.getElementById('content-root')!)
- .render();
-});
\ No newline at end of file
+ ReactDOMClient.createRoot(document.getElementById("content-root")!).render(
+
+ );
+});
diff --git a/api/src/frontend/projectDetails.tsx b/api/src/frontend/projectDetails.tsx
new file mode 100644
index 00000000..502de677
--- /dev/null
+++ b/api/src/frontend/projectDetails.tsx
@@ -0,0 +1,251 @@
+import { timeStamp } from "console";
+import React from "react";
+import ReactDOMClient from "react-dom/client";
+import { Option } from "./components/option";
+import { ProgressBar } from "./components/progressBar";
+import { TagsInput } from "./components/tagsInput";
+import { TextInput } from "./components/textInput";
+import {
+ stepData,
+ scopeData,
+ timeData,
+ nameExamples,
+ industryData,
+ skillData,
+} from "./config/details-data";
+
+export type ProjectDetailsProps = {};
+
+export type ProjectDetailsState = {
+ step: number;
+ info: ProjectInfo;
+};
+
+export type ProjectInfo = {
+ name: string;
+ industry: string[];
+ description: string;
+ scope: string;
+ time: string;
+ skill: string[];
+ budget: number | undefined;
+};
+
+export class ProjectDetails extends React.Component<
+ ProjectDetailsProps,
+ ProjectDetailsState
+> {
+ state = {
+ step: 0,
+ info: {
+ name: "",
+ industry: [],
+ description: "",
+ scope: "",
+ time: "",
+ skill: [],
+ budget: undefined,
+ },
+ };
+ constructor(props: ProjectDetailsProps) {
+ super(props);
+ }
+
+ onBack = () => {
+ const { step } = this.state;
+ step >= 1 && this.setState({ ...this.state, step: step - 1 });
+ };
+
+ onNext = () => {
+ const { step } = this.state;
+ step < stepData.length - 1 &&
+ this.setState({ ...this.state, step: step + 1 });
+ };
+
+ updateFormData = (name: string, value: string | number | string[]) => {
+ this.setState({
+ ...this.state,
+ info: {
+ ...this.state.info,
+ [name]: value,
+ },
+ });
+ };
+
+ render() {
+ const { step } = this.state;
+ const NamePanel = (
+ <>
+ Write a headline for your brief
+
+ this.updateFormData("name", e.target.value)}
+ />
+
+ Examples
+
+ {nameExamples.map((name, index) => (
+
+ {name}
+
+ ))}
+
+ >
+ );
+
+ const IndustryPanel = (
+ <>
+ Search industries or add your own
+
+ this.updateFormData("industry", tags)}
+ />
+
+ >
+ );
+
+ const DescriptionPanel = (
+
+
Describe your project in a few sentences
+
+ this.updateFormData("description", e.target.value)}
+ />
+
+
+ );
+
+ const SkillPanel = (
+ <>
+ Search the skills
+
+ this.updateFormData("skill", tags)}
+ />
+
+ >
+ );
+
+ const ScopePanel = (
+
+ {scopeData.map(({ label, value, description }, index) => (
+
+ ))}
+
+ );
+
+ const TimePanel = (
+
+ {timeData.map(({ label, value }, index) => (
+
+ );
+
+ const BudgetPanel = (
+
+
Maximum project budget (USD)
+
+
this.updateFormData("budget", e.target.value)}
+ />
+
$
+
+
+ You will be able to set milestones which divide your project into
+ manageable phases.
+
+
+ );
+
+ const panels = [
+ NamePanel,
+ IndustryPanel,
+ DescriptionPanel,
+ SkillPanel,
+ ScopePanel,
+ TimePanel,
+ BudgetPanel,
+ ];
+
+ return (
+
+
+
+
{stepData[step].heading}
+ {stepData[step].content.split("\n").map((content, index) => (
+
+ {content}
+
+ ))}
+
+
+
{panels[step] ?? <>>}
+
+ {step >= 1 && (
+
+ )}
+
+
+
+
+ );
+ }
+}
+
+document.addEventListener("DOMContentLoaded", (event) => {
+ ReactDOMClient.createRoot(document.getElementById("project-details")!).render(
+
+ );
+});
diff --git a/api/src/frontend/proposals.tsx b/api/src/frontend/proposals.tsx
index 4dca8a29..b04403cc 100644
--- a/api/src/frontend/proposals.tsx
+++ b/api/src/frontend/proposals.tsx
@@ -6,115 +6,124 @@ import * as config from "./config";
* Models the milestone data that appears in the /proposals/draft form
*/
export type DraftMilestone = {
- name: string;
- percentage_to_unlock: number;
-}
+ name: string;
+ percentage_to_unlock: number;
+};
export type Milestone = DraftMilestone & {
- milestone_index?: number;
- project_id: number;
- is_approved: boolean;
- created: string;
- modified: string;
+ milestone_index?: number;
+ project_id: number;
+ is_approved: boolean;
+ created: string;
+ modified: string;
};
-
export type DraftProposal = {
- name: string;
- logo: string;
- description: string;
- website: string;
- milestones: DraftMilestone[];
- required_funds: number;
- currency_id: number;
- owner: string;
- user_id?: number;
- chain_project_id?: number;
- category?: string | number;
+ name: string;
+ logo: string;
+ description: string;
+ website: string;
+ milestones: DraftMilestone[];
+ required_funds: number;
+ currency_id: number;
+ owner: string;
+ user_id?: number;
+ chain_project_id?: number;
+ category?: string | number;
};
/**
* Models a "project" saved to the db, but not on chain.
*/
export type Proposal = DraftProposal & {
- id: number;
- status: string;
- user_id: number;
- create_block_number?: number;
- created: string;
- modified: string;
- milestones: Milestone[];
-}
+ id: number;
+ status: string;
+ user_id: number;
+ create_block_number?: number;
+ created: string;
+ modified: string;
+ milestones: Milestone[];
+};
-type ProposalsProps = {}
+type ProposalsProps = {};
type ProposalsState = {
- projectsList: Proposal[]
-}
+ projectsList: Proposal[];
+};
const fetchProjects = async () => {
- const resp = await fetch(
- `${config.apiBase}/projects/`,
- {headers: config.getAPIHeaders});
-
- if (resp.ok) {
- return await resp.json();
- } else {
- // probably only 500+ here since this is a listing route
- // this.dispatchEvent(utils.badRouteEvent("server-error"));
- }
-}
+ const resp = await fetch(`${config.apiBase}/projects/`, {
+ headers: config.getAPIHeaders,
+ });
+
+ if (resp.ok) {
+ return await resp.json();
+ } else {
+ // probably only 500+ here since this is a listing route
+ // this.dispatchEvent(utils.badRouteEvent("server-error"));
+ }
+};
class Proposals extends React.Component {
- state: ProposalsState = {
- projectsList: []
- }
-
- constructor(props: ProposalsProps) {
- super(props);
-
- fetchProjects().then((projects) => {
- this.setState({projectsList: projects})
- });
- }
-
- render() {
- return
-
- {this.state.projectsList.map(p =>
-
- )}
-
-
- }
+ state: ProposalsState = {
+ projectsList: [],
+ };
+
+ constructor(props: ProposalsProps) {
+ super(props);
+
+ fetchProjects().then((projects) => {
+ this.setState({ projectsList: projects });
+ });
+ }
+
+ render() {
+ return (
+
+
+ {this.state.projectsList.map((p) => (
+
+ ))}
+
+
+ );
+ }
}
-
type ProposalItemProps = {
- projectId: number,
- imageSrc: string,
- name: string
-}
+ projectId: number;
+ imageSrc: string;
+ name: string;
+};
-type ProposalItemState = {}
-
-class ProposalItem extends React.Component {
- state: ProposalItemState = {}
-
- render() {
- return
- }
+type ProposalItemState = {};
+
+class ProposalItem extends React.Component<
+ ProposalItemProps,
+ ProposalItemState
+> {
+ state: ProposalItemState = {};
+
+ render() {
+ return (
+
+ );
+ }
}
document.addEventListener("DOMContentLoaded", (event) => {
- ReactDOMClient.createRoot(document.getElementById('imbu-proposals')!)
- .render();
-});
\ No newline at end of file
+ ReactDOMClient.createRoot(document.getElementById("imbu-proposals")!).render(
+
+ );
+});
diff --git a/api/src/frontend/webpack.config.js b/api/src/frontend/webpack.config.js
index 59406761..54970cbe 100644
--- a/api/src/frontend/webpack.config.js
+++ b/api/src/frontend/webpack.config.js
@@ -4,7 +4,8 @@ module.exports = {
entry: {
"login": path.resolve(__dirname, "login.tsx"),
"proposals": path.resolve(__dirname, "proposals.tsx"),
- "details": path.resolve(__dirname, "details.tsx")
+ "details": path.resolve(__dirname, "details.tsx"),
+ "projectDetails": path.resolve(__dirname, "projectDetails.tsx")
},
devtool: process.env.NODE_ENV === "development"
? "inline-source-map"