diff --git a/package-lock.json b/package-lock.json index 08f7034f8..daa6a5c6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5157,9 +5157,13 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001159", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz", - "integrity": "sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA==" + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } }, "node_modules/capture-exit": { "version": "2.0.0", @@ -19617,9 +19621,9 @@ } }, "node_modules/ssri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", - "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "dependencies": { "minipass": "^3.1.1" }, @@ -21903,6 +21907,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -22268,6 +22273,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -22753,9 +22759,9 @@ } }, "node_modules/webpack/node_modules/ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dependencies": { "figgy-pudding": "^3.5.1" } @@ -27806,9 +27812,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001159", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz", - "integrity": "sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA==" + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==" }, "capture-exit": { "version": "2.0.0", @@ -39583,9 +39589,9 @@ } }, "ssri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", - "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "requires": { "minipass": "^3.1.1" } @@ -41783,9 +41789,9 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "requires": { "figgy-pudding": "^3.5.1" } diff --git a/src/apis/projects/api.ts b/src/apis/projects/api.ts new file mode 100644 index 000000000..a443f83e6 --- /dev/null +++ b/src/apis/projects/api.ts @@ -0,0 +1,13 @@ +import axios from 'axios'; + +import {PaginatedResponse} from 'types/api'; +import {Project} from 'types/projects'; + +export async function getProjects(): Promise { + const response = await axios.get>(`${process.env.REACT_APP_BACKEND_API}/projects`); + + if (!response.data) { + throw new Error('Error while fetching projects, please try again.'); + } + return response.data.results; +} diff --git a/src/apis/projects/index.ts b/src/apis/projects/index.ts new file mode 100644 index 000000000..1a615d19f --- /dev/null +++ b/src/apis/projects/index.ts @@ -0,0 +1 @@ +export * as api from './api'; diff --git a/src/apis/tutorials/index.ts b/src/apis/tutorials/index.ts index cb3de4609..5f3e2e45b 100644 --- a/src/apis/tutorials/index.ts +++ b/src/apis/tutorials/index.ts @@ -1,12 +1,11 @@ import axios from 'axios'; import {allTutorialsFilter} from 'constants/tutorials'; +import {PaginatedResponse} from 'types/api'; import {Playlist, PlaylistCategory, Instructor} from 'types/tutorials'; import {standardHeaders} from 'utils/requests'; -import {PlaylistCategoriesResponse, PlaylistsResponse} from './types'; - export async function getPlaylistCategories(): Promise { - const response = await axios.get( + const response = await axios.get>( `${process.env.REACT_APP_BACKEND_API}/playlist_categories`, standardHeaders(), ); @@ -16,7 +15,7 @@ export async function getPlaylistCategories(): Promise { export async function getPlaylists(category: string): Promise { if (category !== allTutorialsFilter.title) { - const response = await axios.get( + const response = await axios.get>( `${process.env.REACT_APP_BACKEND_API}/playlists?category=${category}`, standardHeaders(), ); @@ -24,7 +23,7 @@ export async function getPlaylists(category: string): Promise { return response.data.results; } - const response = await axios.get( + const response = await axios.get>( `${process.env.REACT_APP_BACKEND_API}/playlists`, standardHeaders(), ); diff --git a/src/apis/tutorials/types.ts b/src/apis/tutorials/types.ts deleted file mode 100644 index eae182424..000000000 --- a/src/apis/tutorials/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Playlist, PlaylistCategory} from 'types/tutorials'; - -export interface PlaylistCategoriesResponse { - count: number; - next: number | null; - previous: number | null; - results: PlaylistCategory[]; -} - -export interface PlaylistsResponse { - count: number; - next: number | null; - previous: number | null; - results: Playlist[]; -} diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts index aca82e2d9..8ec4b2c66 100644 --- a/src/apis/users/index.ts +++ b/src/apis/users/index.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import {standardHeaders} from 'utils/requests'; +import {User} from 'types/app/User'; export async function createUser({ display_name, @@ -13,3 +14,11 @@ export async function createUser({ }) { return axios.post(`${process.env.REACT_APP_BACKEND_API}/users`, {display_name, email, password}, standardHeaders()); } + +export async function getUser({uuid}: {uuid: string}): Promise { + const response = await axios.get(`${process.env.REACT_APP_BACKEND_API}/users/${uuid}`); + if (!response.data) { + throw new Error('Error while fetching user. Please try again.'); + } + return response.data; +} diff --git a/src/components/BreadcrumbMenu/BreadcrumbMenu.scss b/src/components/BreadcrumbMenu/BreadcrumbMenu.scss index 667464917..9fb22e915 100644 --- a/src/components/BreadcrumbMenu/BreadcrumbMenu.scss +++ b/src/components/BreadcrumbMenu/BreadcrumbMenu.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + $navigation-bars-height: 100px; // TopNav.height + BreadcrumbMenu__bar.height .BreadcrumbMenu { @@ -31,7 +33,7 @@ $navigation-bars-height: 100px; // TopNav.height + BreadcrumbMenu__bar.height right: 0; top: $navigation-bars-height; width: 100vw; - z-index: 2; + z-index: $z-index-breadcrumb; } &__navigation { diff --git a/src/components/FormElements/Button/Button.scss b/src/components/FormElements/Button/Button.scss index 1a3821110..bfed388ab 100644 --- a/src/components/FormElements/Button/Button.scss +++ b/src/components/FormElements/Button/Button.scss @@ -1,11 +1,11 @@ .Button { $self: &; - border-radius: 3px; + border-radius: 100px; border-style: solid; border-width: 1px; - box-shadow: 0 4px 4px rgba(4, 34, 53, 0.25); + box-shadow: none; // currently needed to override @thenewboston/ui 's css cursor: pointer; - padding: 9px 12px; + padding: 11px 12px; transition: all 0.1s; &--disabled { @@ -57,11 +57,11 @@ background: transparent; &#{$self}--primary { - border-color: var(--color-primary); + border-color: var(--color-gray-200); color: var(--color-primary); &:hover { - background: var(--color-primary); + background: var(--color-gray-100); } &#{$self}--disabled, diff --git a/src/components/GoToTop/GoToTop.scss b/src/components/GoToTop/GoToTop.scss index 4c33d5769..5f89563c3 100644 --- a/src/components/GoToTop/GoToTop.scss +++ b/src/components/GoToTop/GoToTop.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + @keyframes fadeIn { from { opacity: 0; @@ -19,5 +21,5 @@ position: fixed; right: 24px; width: 36px; - z-index: 1000; + z-index: $z-index-go-to-top; } diff --git a/src/components/Layout/Layout.scss b/src/components/Layout/Layout.scss index 88b73d724..336090f4c 100644 --- a/src/components/Layout/Layout.scss +++ b/src/components/Layout/Layout.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + $top-nav-height: 60px; $footer-height: 348px; @@ -30,6 +32,6 @@ $footer-height: 348px; &__top-nav-wrapper { position: sticky; top: 0; - z-index: 1; + z-index: $z-index-layout; } } diff --git a/src/components/Modal/Modal.scss b/src/components/Modal/Modal.scss index 6d2f86e46..9a9c88acb 100644 --- a/src/components/Modal/Modal.scss +++ b/src/components/Modal/Modal.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + $modal-header-height: 48px; $modal-header-height-mobile: 75px; $modal-footer-height: 60px; @@ -100,7 +102,7 @@ $break-mobile: 640px; height: 100%; position: fixed; width: 100vw; - z-index: 20; + z-index: $z-index-modal; &__close-icon { left: 23px; diff --git a/src/components/SlideUp/SlideUp.scss b/src/components/SlideUp/SlideUp.scss index 8d70e382e..4dc4a24fc 100644 --- a/src/components/SlideUp/SlideUp.scss +++ b/src/components/SlideUp/SlideUp.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + @keyframes addOverlay { from { background: rgba(0, 0, 0, 0); @@ -11,7 +13,7 @@ bottom: 0; position: absolute; width: 100vw; - z-index: 2; + z-index: $z-index-slideup; &__content { background: var(--color-white); @@ -26,6 +28,6 @@ position: fixed; top: 0; width: 100vw; - z-index: 2; + z-index: $z-index-slideup-overlay; } } diff --git a/src/components/index.ts b/src/components/index.ts index 1b06707dc..670d6fb2c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -35,7 +35,6 @@ import Loader from './Loader'; import PageTitle from './PageTitle'; import Pagination from './Pagination'; import Popover from './Popover'; -import ProjectsMenuItems from './ProjectsMenuItems'; import Qr from './Qr'; import {QueryParamsOffsetAndLimit} from './QueryParams'; import Shadow from './Shadow'; @@ -84,7 +83,6 @@ export { PageTitle, Pagination, Popover, - ProjectsMenuItems, Qr, QueryParamsOffsetAndLimit, RequestResponseSnippet, diff --git a/src/containers/App.tsx b/src/containers/App.tsx index a610d8021..4f1d629b6 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -21,6 +21,7 @@ import PrimaryValidatorApi from './PrimaryValidatorApi'; import Profile from './Profile'; import Progress from './Progress'; import Projects from './Projects'; +import ProjectRulesAndGuide from './Projects/ProjectRulesAndGuide'; import SignIn from './SignIn'; import SignOut from './SignOut'; import Social from './Social'; @@ -84,7 +85,8 @@ const App: FC = () => { - + + diff --git a/src/containers/Projects/ListOfProjects/ListOfProjects.scss b/src/containers/Projects/ListOfProjects/ListOfProjects.scss new file mode 100644 index 000000000..12c2479a7 --- /dev/null +++ b/src/containers/Projects/ListOfProjects/ListOfProjects.scss @@ -0,0 +1,28 @@ +@import '../shared'; + +.ListOfProjects { + column-gap: 32px; + display: grid; + grid-template-columns: minmax($card-min-width-original, $card-width-original) minmax($card-min-width-original, $card-width-original) + minmax($card-min-width-original, $card-width-original); + justify-content: center; + justify-items: center; + margin-bottom: 32px; + row-gap: 32px; + + @media (max-width: 992px) { + grid-template-columns: minmax($card-min-width-992, $card-width-992) minmax($card-min-width-992, $card-width-992); + } + + @media (max-width: 768px) { + grid-template-columns: minmax($card-min-width-768, $card-width-768) minmax($card-min-width-768, $card-width-768); + } + + @media (max-width: 480px) { + grid-template-columns: $card-width-480; + } + + @media (max-width: 376px) { + grid-template-columns: max-content; + } +} diff --git a/src/containers/Projects/ListOfProjects/index.tsx b/src/containers/Projects/ListOfProjects/index.tsx new file mode 100644 index 000000000..c28f3932b --- /dev/null +++ b/src/containers/Projects/ListOfProjects/index.tsx @@ -0,0 +1,31 @@ +import React, {FC} from 'react'; + +import {Project} from 'types/projects'; + +import ProjectCard from '../ProjectCard'; +import './ListOfProjects.scss'; + +type Props = { + projects: Project[]; +}; + +const ListOfProjects: FC = ({projects}) => { + return ( +
+ {projects.map(({description, logo, project_lead_display_name: projectLeadDisplayName, title, pk}) => { + return ( + + ); + })} +
+ ); +}; + +export default ListOfProjects; diff --git a/src/containers/Projects/ProjectCard/ProjectCard.scss b/src/containers/Projects/ProjectCard/ProjectCard.scss new file mode 100644 index 000000000..0e0828ef4 --- /dev/null +++ b/src/containers/Projects/ProjectCard/ProjectCard.scss @@ -0,0 +1,138 @@ +@import 'styles/font-mixins'; +@import '../shared'; + +$padding: 40px; +$padding-768: 24px; + +.ProjectCard { + border-radius: 12px; + box-shadow: 0 0 30px 0 #e5eaf47a; + height: $card-height-original; + max-width: $card-width-original; + padding: $padding; + position: relative; + width: 100%; + + &:hover { + box-shadow: 0 7px 40px 0 #e9effd; + } + + @media (max-width: 992px) { + max-width: $card-width-992; + } + + @media (max-width: 768px) { + height: $card-height-768; + max-width: $card-width-768; + padding: $padding-768; + } + + @media (max-width: 480px) { + height: $card-height-480; + max-width: $card-width-480; + } + + @media (max-width: 376px) { + max-width: 100vw; + } + + &__title-container { + display: flex; + flex-direction: column; + } + + &__avatar { + margin-right: 20px; + + @media (max-width: 768px) { + margin-right: 20px; + } + } + + &__top-container { + align-items: center; + display: flex; + flex-direction: row; + margin-bottom: 35px; + + @media (max-width: 768px) { + flex-direction: column; + text-align: center; + } + } + + &__details-button { + @extend %body1; + align-items: center; + bottom: $padding; + display: flex; + font-weight: var(--font-weight-regular); + position: absolute; + right: $padding; + + @media (max-width: 768px) { + bottom: $padding-768; + left: 50%; + transform: translateX(-50%); + } + } + + &__details-icon { + margin-left: 6px; + } + + &__description { + @extend %h3; + -webkit-box-orient: vertical; + color: var(--color-sail-gray-400); + display: -webkit-box; + font-weight: var(--font-weight-regular); + -webkit-line-clamp: 4; /* max number of lines to show */ + overflow: hidden; + text-overflow: ellipsis; + + @media (max-width: 768px) { + -webkit-line-clamp: 3; /* max number of lines to show */ + text-align: center; + } + + @media (max-width: 480px) { + -webkit-line-clamp: 6; /* max number of lines to show */ + } + } + + &__project-lead { + @extend %h4; + color: var(--color-sail-gray-300); + font-weight: var(--font-weight-medium); + } + + &__project-lead-container { + @media (max-width: 768px) { + align-self: center; + } + } + + &__project-lead-name { + @extend %h4; + color: var(--color-sail-gray-700); + font-weight: var(--font-weight-medium); + } + + &__project-title { + @extend %h1; + + -webkit-box-orient: vertical; + color: var(--color-blue-900); + display: -webkit-box; + font-weight: var(--font-weight-bold); + -webkit-line-clamp: 1; /* max number of lines to show */ + margin-bottom: 10px; + overflow: hidden; + text-overflow: ellipsis; + + @media (max-width: 768px) { + margin-bottom: 2px; + } + } +} diff --git a/src/containers/Projects/ProjectCard/index.tsx b/src/containers/Projects/ProjectCard/index.tsx new file mode 100644 index 000000000..61a99d127 --- /dev/null +++ b/src/containers/Projects/ProjectCard/index.tsx @@ -0,0 +1,47 @@ +import React, {FC} from 'react'; +import {useHistory} from 'react-router-dom'; + +import {Avatar, Button} from 'components'; +import {useWindowDimensions} from 'hooks'; +import {Icon, IconType} from '@thenewboston/ui'; + +import './ProjectCard.scss'; + +type Props = { + description: string; + id: string; + logoUrl: string; + projectLeadDisplayName: string; + title: string; +}; + +const ProjectCard: FC = ({description, id, logoUrl, projectLeadDisplayName, title}) => { + const history = useHistory(); + const {width} = useWindowDimensions(); + + const handleButtonClick = (): void => { + history.push(`/projects/${id}`); + }; + + return ( +
+
+ 768 ? 64 : 20} /> +
+

{title}

+
+ Project Lead: + {projectLeadDisplayName} +
+
+
+
{description}
+ +
+ ); +}; + +export default ProjectCard; diff --git a/src/containers/Projects/ProjectDetails/ProjectDetails.scss b/src/containers/Projects/ProjectDetails/ProjectDetails.scss new file mode 100644 index 000000000..3dcffca67 --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetails.scss @@ -0,0 +1,35 @@ +@import 'styles/font-mixins'; + +.ProjectDetails { + background-color: var(--color-white); + margin-bottom: 160px; + + &__main-container { + display: flex; + flex-direction: row; + } + + &__main-content { + background-color: var(--color-white); + box-shadow: 0 8px 88px 0 #0000000d; + margin-bottom: 120px; + padding: 32px; + } + + &__milestone { + color: var(--color-sail-gray-600); + } + + &__milestone-description { + @extend %h3; + color: var(--color-sail-gray-600); + font-weight: var(--font-weight-regular); + margin-bottom: 40px; + white-space: pre-line; + } + + &__milestone-number { + font-weight: var(--font-weight-bold); + margin-bottom: 8px; + } +} diff --git a/src/containers/Projects/ProjectDetails/ProjectDetailsHeader/ProjectDetailsHeader.scss b/src/containers/Projects/ProjectDetails/ProjectDetailsHeader/ProjectDetailsHeader.scss new file mode 100644 index 000000000..ef004f04a --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetailsHeader/ProjectDetailsHeader.scss @@ -0,0 +1,95 @@ +@import '../../shared'; +@import 'styles/z-index'; + +.ProjectDetailsHeader { + align-items: center; + background: rgba(255, 255, 255, 0.9); + display: flex; + flex-direction: row; + height: $details-header-height; + padding: 47px; + position: sticky; + top: $top-nav-height; + z-index: $z-index-project-header; + + @media (max-width: 480px) { + align-items: center; + flex-direction: column; + height: $details-header-height-480; + padding: 24px; + } + + &__github-button { + align-items: center; + display: flex; + } + + &__github-icon { + margin-right: 6px; + } + + &__left-container { + align-items: center; + display: flex; + flex-direction: row; + + @media (max-width: 480px) { + margin-bottom: 30px; + } + } + + &__main-container { + display: flex; + justify-content: space-between; + margin-left: 22px; + width: 100%; + + @media (max-width: 480px) { + align-items: center; + flex-direction: column; + margin-left: 0; + } + } + + &__avatar { + @media (max-width: 480px) { + margin-bottom: 25px; + } + } + + &__title-container { + display: flex; + flex-direction: column; + + @media (max-width: 480px) { + text-align: center; + } + } + + &__project-lead-container { + max-width: 500px; + overflow: hidden; + text-overflow: ellipsis; + + @media (max-width: 992px) { + max-width: 350px; + } + + @media (max-width: 578px) { + max-width: 250px; + } + } + + &__project-lead { + color: var(--color-sail-gray-300); + } + + &__project-lead-name { + color: var(--color-sail-gray-700); + } + + &__project-title { + color: var(--color-blue-900); + margin-bottom: 10px; + } +} diff --git a/src/containers/Projects/ProjectDetails/ProjectDetailsHeader/index.tsx b/src/containers/Projects/ProjectDetails/ProjectDetailsHeader/index.tsx new file mode 100644 index 000000000..e7388d311 --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetailsHeader/index.tsx @@ -0,0 +1,43 @@ +import React, {FC} from 'react'; +import {Icon, IconType} from '@thenewboston/ui'; + +import {Avatar, Button} from 'components'; +import './ProjectDetailsHeader.scss'; + +type Props = { + github: string; + logoUrl: string; + projectLeadDisplayName: string; + title: string; +}; + +const ProjectDetailsHeader: FC = ({github, logoUrl, projectLeadDisplayName, title}) => { + return ( +
+ +
+
+
+

{title}

+
+ Project Lead: + {projectLeadDisplayName} +
+
+
+
+ +
+
+
+ ); +}; + +export default ProjectDetailsHeader; diff --git a/src/containers/Projects/ProjectDetails/ProjectDetailsSideMenu/ProjectDetailsSideMenu.scss b/src/containers/Projects/ProjectDetails/ProjectDetailsSideMenu/ProjectDetailsSideMenu.scss new file mode 100644 index 000000000..75dcf2f69 --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetailsSideMenu/ProjectDetailsSideMenu.scss @@ -0,0 +1,56 @@ +@import 'styles/font-mixins'; +@import '../../shared'; + +.ProjectDetailsSideMenu { + background-color: var(--color-white); + border-radius: 0 0 0 12px; + box-shadow: 0 8px 88px rgba(0, 0, 0, 0.05); + flex-shrink: 0; + height: fit-content; + left: 0; + padding: 32px; + position: sticky; + top: $top-content-height; + width: 438px; + + @media (max-width: 1200px) { + width: 376px; + } + + @media (max-width: 992px) { + padding: 24px; + width: 72px; + } + + @media (max-width: 480px) { + top: $top-content-height-480; + } + + &__topic { + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + margin-bottom: 32px; + outline: none; + } + + &__icon { + margin-right: 18px; + } + + &__topic-title { + @extend %h3; + color: var(--color-gray-500); + font-weight: var(--font-weight-regular); + + &:hover { + color: var(--color-cyan-800); + } + + &--active { + color: var(--color-cyan-800); + font-weight: var(--font-weight-bold); + } + } +} diff --git a/src/containers/Projects/ProjectDetails/ProjectDetailsSideMenu/index.tsx b/src/containers/Projects/ProjectDetails/ProjectDetailsSideMenu/index.tsx new file mode 100644 index 000000000..334d85ddc --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetailsSideMenu/index.tsx @@ -0,0 +1,89 @@ +import React, {FC, useState} from 'react'; +import {useHistory} from 'react-router-dom'; +import clsx from 'clsx'; + +import {useWindowDimensions} from 'hooks'; +import {ProjectTopic} from 'types/projects'; +import {isTouchScreenDevice} from 'utils/device'; + +import ProjectIcon, {ProjectIconSize} from '../../ProjectIcons'; +import {orderedProjectDetailsTopic} from '../constants'; +import './ProjectDetailsSideMenu.scss'; + +type Props = { + currentTopicPosition: number; + setCurrentTopicPosition(position: number): void; +}; + +const ProjectDetailsSideMenu: FC = ({currentTopicPosition, setCurrentTopicPosition}) => { + const history = useHistory(); + const [hoveredTopicTitle, setHoveredTopicTitle] = useState(''); + const currentTopic = orderedProjectDetailsTopic[currentTopicPosition]; + + const {width} = useWindowDimensions(); + const shouldShowDetails = width > 992; + + const handleMenuClick = (topic: ProjectTopic) => (): void => { + setCurrentTopicPosition(topic.position); + history.push(`#${topic.anchor}`); + }; + + const handleMouseEnter = (title: string) => { + // touch screen devices does not need to have hover effect + if (isTouchScreenDevice()) { + return; + } + setHoveredTopicTitle(title); + }; + + const handleMouseLeave = () => { + setHoveredTopicTitle(''); + }; + + return ( +
+ {orderedProjectDetailsTopic.map((topic) => { + const {iconType, title} = topic; + const isActive = currentTopic.title === title; + const isHovered = hoveredTopicTitle === title; + + let iconState: 'default' | 'active' | 'hover' = 'default'; + if (isActive) { + iconState = 'active'; + } else if (isHovered) { + iconState = 'hover'; + } + + return ( +
handleMouseEnter(title)} + onMouseLeave={handleMouseLeave} + tabIndex={0} + > + + {shouldShowDetails && ( +
+ {title} +
+ )} +
+ ); + })} +
+ ); +}; + +export default ProjectDetailsSideMenu; diff --git a/src/containers/Projects/ProjectDetails/ProjectDetailsTopic/ProjectDetailsTopic.scss b/src/containers/Projects/ProjectDetails/ProjectDetailsTopic/ProjectDetailsTopic.scss new file mode 100644 index 000000000..f332511f6 --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetailsTopic/ProjectDetailsTopic.scss @@ -0,0 +1,61 @@ +@import 'styles/font-mixins'; +$top-nav-height: 60px; +$details-container-height: 158px; +$details-container-height-480: 241px; +$wiggle-room: 32px; +$offset: $top-nav-height + $details-container-height + $wiggle-room; +$offset-480: $top-nav-height + $details-container-height-480 + $wiggle-room; + +.ProjectDetailsTopic { + align-items: flex-start; + display: flex; + flex-direction: row; + margin-bottom: 120px; + position: relative; + + @media (max-width: 480px) { + flex-direction: column; + } + + &__anchor { + position: absolute; + top: #{-$offset}; + + @media (max-width: 480px) { + top: #{-$offset-480}; + } + } + + &__icon { + margin-right: 32px; + + @media (max-width: 480px) { + margin-bottom: 15px; + } + } + + &__content { + display: flex; + flex-direction: column; + + &-main { + @extend %h3; + color: var(--color-sail-gray-600); + font-weight: var(--font-weight-regular); + white-space: pre-line; + } + + &-overview { + @extend %body1; + color: var(--color-sail-gray-500); + font-weight: var(--font-weight-medium); + margin-bottom: 32px; + } + + &-title { + color: var(--color-sail-blue-900); + font-weight: var(--font-weight-bold); + margin-bottom: 8px; + } + } +} diff --git a/src/containers/Projects/ProjectDetails/ProjectDetailsTopic/index.tsx b/src/containers/Projects/ProjectDetails/ProjectDetailsTopic/index.tsx new file mode 100644 index 000000000..58a3f2378 --- /dev/null +++ b/src/containers/Projects/ProjectDetails/ProjectDetailsTopic/index.tsx @@ -0,0 +1,41 @@ +import React, {forwardRef} from 'react'; +import {useHistory, useLocation} from 'react-router-dom'; + +import ProjectIcon, {ProjectIconSize, ProjectIconType} from '../../ProjectIcons'; +import './ProjectDetailsTopic.scss'; + +interface ComponentProps { + content: React.ReactNode; + iconType: ProjectIconType; + id: string; + overview: string; + title: string; +} + +const ProjectDetailsTopic = forwardRef( + ({content, iconType, id, title, overview}, ref) => { + const history = useHistory(); + const {pathname} = useLocation(); + + return ( +
+ +
+
+

history.push(`${pathname}#${id}`)}> + {title} +

+

{overview}

+
{content}
+
+
+ ); + }, +); + +export default ProjectDetailsTopic; diff --git a/src/containers/Projects/ProjectDetails/constants.ts b/src/containers/Projects/ProjectDetails/constants.ts new file mode 100644 index 000000000..2f245a594 --- /dev/null +++ b/src/containers/Projects/ProjectDetails/constants.ts @@ -0,0 +1,59 @@ +import {ProjectTopicAnchor, ProjectTopicMap, ProjectTopicTitle} from 'types/projects'; +import {sortByNumberKey} from 'utils/sort'; +import {ProjectIconType} from '../ProjectIcons'; + +export const projectDetailsTopic: ProjectTopicMap = { + benefits: { + anchor: ProjectTopicAnchor.Benefits, + iconType: ProjectIconType.Benefits, + overview: + 'A brief description, how this service would bring the people together and providing opportunities of interaction', + position: 3, + title: ProjectTopicTitle.Benefits, + }, + centered_around_tnb: { + anchor: ProjectTopicAnchor.CenteredAroundTNB, + iconType: ProjectIconType.Integration, + overview: 'A brief description how this service would blend into the TNB services.', + position: 4, + title: ProjectTopicTitle.CenteredAroundTNB, + }, + estimated_completion_date: { + anchor: ProjectTopicAnchor.EstimatedCompletionDate, + iconType: ProjectIconType.Timeline, + overview: 'A specific date of project completion.', + position: 5, + title: ProjectTopicTitle.EstimatedCompletionDate, + }, + overview: { + anchor: ProjectTopicAnchor.Overview, + iconType: ProjectIconType.Overview, + overview: + 'A brief summary, situation, plan, and outline about the project, bigger picture, functionality and the possible outcome from this project', + position: 0, + title: ProjectTopicTitle.Overview, + }, + problem: { + anchor: ProjectTopicAnchor.Problem, + iconType: ProjectIconType.Problem, + overview: 'A precise information about the problem that this project is going to solve.', + position: 1, + title: ProjectTopicTitle.Problem, + }, + roadmap: { + anchor: ProjectTopicAnchor.Roadmap, + iconType: ProjectIconType.Roadmap, + overview: 'A schedule of a lengthy project by breaking into realistic achiveable milestones.', + position: 6, + title: ProjectTopicTitle.Roadmap, + }, + target_market: { + anchor: ProjectTopicAnchor.TargetMarket, + iconType: ProjectIconType.Target, + overview: 'A description of the group of users to whom TNB wants to provide the service.', + position: 2, + title: ProjectTopicTitle.TargetMarket, + }, +}; + +export const orderedProjectDetailsTopic = Object.values(projectDetailsTopic).sort(sortByNumberKey('position')); diff --git a/src/containers/Projects/ProjectDetails/index.tsx b/src/containers/Projects/ProjectDetails/index.tsx new file mode 100644 index 000000000..435c6a5ad --- /dev/null +++ b/src/containers/Projects/ProjectDetails/index.tsx @@ -0,0 +1,189 @@ +import React, {FC, ReactNode, useEffect, useRef, useState} from 'react'; +import {useLocation} from 'react-router-dom'; +import format from 'date-fns/format'; +import parseISO from 'date-fns/parseISO'; +import throttle from 'lodash/throttle'; + +import {useEventListener, useWindowDimensions} from 'hooks'; +import {Project} from 'types/projects'; + +import {projectDetailsTopic} from './constants'; +import ProjectDetailsHeader from './ProjectDetailsHeader'; +import ProjectDetailsSideMenu from './ProjectDetailsSideMenu'; +import ProjectDetailsTopic from './ProjectDetailsTopic'; +import './ProjectDetails.scss'; + +type Props = { + project: Project; +}; + +const TOP_NAV_HEIGHT = 60; +const DETAILS_CONTAINER_HEIGHT = 158; +const DETAILS_CONTAINER_HEIGHT_480 = 241; +const WIGGLE_ROOM = 64; + +let debounce = false; + +const ProjectDetails: FC = ({project}) => { + const {hash} = useLocation(); + + const overviewRef = useRef(null); + const problemRef = useRef(null); + const targetMarketRef = useRef(null); + const benefitsRef = useRef(null); + const centeredAroundTnbRef = useRef(null); + const estimatedCompletionDateRef = useRef(null); + const roadmapRef = useRef(null); + + const problemOffset = problemRef.current?.offsetTop || -1; + const targetMarketOffset = targetMarketRef.current?.offsetTop || -1; + const benefitsOffset = benefitsRef.current?.offsetTop || -1; + const centeredAroundTnbOffset = centeredAroundTnbRef.current?.offsetTop || -1; + const estimatedCompletionDateOffset = estimatedCompletionDateRef.current?.offsetTop || -1; + const roadmapOffset = roadmapRef.current?.offsetTop || -1; + + const [currentTopicPosition, setCurrentTopicPosition] = useState(0); + const {title, logo, github_url: github, project_lead_display_name: projectLeadDisplayName} = project; + + const {width} = useWindowDimensions(); + const detailsHeaderHeight = width >= 480 ? DETAILS_CONTAINER_HEIGHT : DETAILS_CONTAINER_HEIGHT_480; + + // This is used so that the hash change does not trigger the scroll event listener + useEffect(() => { + debounce = true; + setTimeout(() => { + debounce = false; + }, 100); + }, [hash]); + + useEventListener( + 'scroll', + throttle(() => { + if (debounce) return; + + const scrollHeight = window.scrollY + TOP_NAV_HEIGHT + detailsHeaderHeight + WIGGLE_ROOM; + + if (scrollHeight > roadmapOffset) { + setCurrentTopicPosition(6); + return; + } + if (scrollHeight > estimatedCompletionDateOffset) { + setCurrentTopicPosition(5); + return; + } + if (scrollHeight > centeredAroundTnbOffset) { + setCurrentTopicPosition(4); + return; + } + if (scrollHeight > benefitsOffset) { + setCurrentTopicPosition(3); + return; + } + if (scrollHeight > targetMarketOffset) { + setCurrentTopicPosition(2); + return; + } + if (scrollHeight > problemOffset) { + setCurrentTopicPosition(1); + return; + } + setCurrentTopicPosition(0); + }, 100), + window, + true, + ); + + const renderMainContent = (): ReactNode => { + return ( +
+ + + + + + + +
+ ); + }; + + const renderMilestones = (): ReactNode => { + return project.milestones.map((milestone) => { + return ( +
+

Milestone {milestone.number}

+
{milestone.description}
+
+ ); + }); + }; + + return ( +
+ +
+ + {renderMainContent()} +
+
+ ); +}; + +export default ProjectDetails; diff --git a/src/containers/Projects/ProjectIcons/BenefitsIcon.tsx b/src/containers/Projects/ProjectIcons/BenefitsIcon.tsx new file mode 100644 index 000000000..6b18b7538 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/BenefitsIcon.tsx @@ -0,0 +1,324 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const BenefitsIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default BenefitsIcon; diff --git a/src/containers/Projects/ProjectIcons/IntegrationIcon.tsx b/src/containers/Projects/ProjectIcons/IntegrationIcon.tsx new file mode 100644 index 000000000..8901b6e83 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/IntegrationIcon.tsx @@ -0,0 +1,72 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const IntegrationIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + ); +}; + +export default IntegrationIcon; diff --git a/src/containers/Projects/ProjectIcons/MilestonesIcon.tsx b/src/containers/Projects/ProjectIcons/MilestonesIcon.tsx new file mode 100644 index 000000000..8a01c55dc --- /dev/null +++ b/src/containers/Projects/ProjectIcons/MilestonesIcon.tsx @@ -0,0 +1,82 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const MilestonesIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + + + + ); +}; + +export default MilestonesIcon; diff --git a/src/containers/Projects/ProjectIcons/OverviewIcon.tsx b/src/containers/Projects/ProjectIcons/OverviewIcon.tsx new file mode 100644 index 000000000..50efbd353 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/OverviewIcon.tsx @@ -0,0 +1,240 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const OverviewIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default OverviewIcon; diff --git a/src/containers/Projects/ProjectIcons/ProblemIcon.tsx b/src/containers/Projects/ProjectIcons/ProblemIcon.tsx new file mode 100644 index 000000000..001d45c70 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/ProblemIcon.tsx @@ -0,0 +1,114 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const ProblemIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProblemIcon; diff --git a/src/containers/Projects/ProjectIcons/RoadmapIcon.tsx b/src/containers/Projects/ProjectIcons/RoadmapIcon.tsx new file mode 100644 index 000000000..06efded5d --- /dev/null +++ b/src/containers/Projects/ProjectIcons/RoadmapIcon.tsx @@ -0,0 +1,69 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const RoadmapIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + + + + ); +}; + +export default RoadmapIcon; diff --git a/src/containers/Projects/ProjectIcons/TargetIcon.tsx b/src/containers/Projects/ProjectIcons/TargetIcon.tsx new file mode 100644 index 000000000..5998aeeaa --- /dev/null +++ b/src/containers/Projects/ProjectIcons/TargetIcon.tsx @@ -0,0 +1,138 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const TargetIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TargetIcon; diff --git a/src/containers/Projects/ProjectIcons/TimelineIcon.tsx b/src/containers/Projects/ProjectIcons/TimelineIcon.tsx new file mode 100644 index 000000000..d35a01b38 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/TimelineIcon.tsx @@ -0,0 +1,276 @@ +import React, {FC} from 'react'; + +import {CustomIconProps} from './types'; + +const TimelineIcon: FC = ({size, state}) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {state === 'default' && ( + + + + )} + + + + + + + + ); +}; + +export default TimelineIcon; diff --git a/src/containers/Projects/ProjectIcons/index.tsx b/src/containers/Projects/ProjectIcons/index.tsx new file mode 100644 index 000000000..aa57587d3 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/index.tsx @@ -0,0 +1,85 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, {FC, ReactNode, useCallback} from 'react'; +import clsx from 'clsx'; + +import BenefitsIcon from './BenefitsIcon'; +import IntegrationIcon from './IntegrationIcon'; +import MilestonesIcon from './MilestonesIcon'; +import OverviewIcon from './OverviewIcon'; +import ProblemIcon from './ProblemIcon'; +import RoadmapIcon from './RoadmapIcon'; +import TargetIcon from './TargetIcon'; +import TimelineIcon from './TimelineIcon'; +import {CustomIconProps} from './types'; + +export enum ProjectIconType { + Benefits = 'Benefits', + Integration = 'Integration', + Milestone = 'Milestone', + Overview = 'Overview', + Problem = 'Problem', + Roadmap = 'Roadmap', + Target = 'Target', + Timeline = 'Timeline', +} + +export enum ProjectIconSize { + small = 'small', + medium = 'medium', + large = 'large', +} + +export interface ComponentProps extends CustomIconProps { + className?: string; + dataTestId?: string; + icon: ProjectIconType; + size: ProjectIconSize; +} + +const ProjectIcon: FC = ({className, dataTestId, icon, size, state}) => { + const renderIcon = useCallback((): ReactNode => { + let sizeNumber: number; + if (size === ProjectIconSize.small) { + sizeNumber = 24; + } else if (size === ProjectIconSize.medium) { + sizeNumber = 32; + } else { + sizeNumber = 96; + } + + const iconProps = { + 'data-testid': 'ProjectIcon__svg', + size: sizeNumber, + state, + }; + + switch (icon) { + case ProjectIconType.Benefits: + return ; + case ProjectIconType.Integration: + return ; + case ProjectIconType.Milestone: + return ; + case ProjectIconType.Overview: + return ; + case ProjectIconType.Problem: + return ; + case ProjectIconType.Roadmap: + return ; + case ProjectIconType.Target: + return ; + case ProjectIconType.Timeline: + return ; + default: + return null; + } + }, [icon, size, state]); + + return ( +
+ {renderIcon()} +
+ ); +}; + +export default ProjectIcon; diff --git a/src/containers/Projects/ProjectIcons/types.ts b/src/containers/Projects/ProjectIcons/types.ts new file mode 100644 index 000000000..2948246d1 --- /dev/null +++ b/src/containers/Projects/ProjectIcons/types.ts @@ -0,0 +1,4 @@ +export interface CustomIconProps { + size: number | string; + state: 'default' | 'hover' | 'active'; +} diff --git a/src/containers/Projects/ProjectsMilestones/ProjectsMilestones.scss b/src/containers/Projects/ProjectRulesAndGuide/ProjectsMilestones/ProjectsMilestones.scss similarity index 85% rename from src/containers/Projects/ProjectsMilestones/ProjectsMilestones.scss rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsMilestones/ProjectsMilestones.scss index a85a9355e..5166da032 100644 --- a/src/containers/Projects/ProjectsMilestones/ProjectsMilestones.scss +++ b/src/containers/Projects/ProjectRulesAndGuide/ProjectsMilestones/ProjectsMilestones.scss @@ -1,3 +1,5 @@ +@import 'styles/font-mixins'; + .ProjectsMilestones { &__caption { margin: 12px 0; @@ -27,9 +29,9 @@ } &__text { + @extend %h3; color: var(--color-primary); - font-size: var(--font-size-h3); - font-weight: normal; + font-weight: var(--font-weight-regular); } } diff --git a/src/containers/Projects/ProjectsMilestones/index.tsx b/src/containers/Projects/ProjectRulesAndGuide/ProjectsMilestones/index.tsx similarity index 100% rename from src/containers/Projects/ProjectsMilestones/index.tsx rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsMilestones/index.tsx diff --git a/src/containers/Projects/ProjectsOverview/RoadmapMilestoneOverview.png b/src/containers/Projects/ProjectRulesAndGuide/ProjectsOverview/RoadmapMilestoneOverview.png similarity index 100% rename from src/containers/Projects/ProjectsOverview/RoadmapMilestoneOverview.png rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsOverview/RoadmapMilestoneOverview.png diff --git a/src/containers/Projects/ProjectsOverview/index.tsx b/src/containers/Projects/ProjectRulesAndGuide/ProjectsOverview/index.tsx similarity index 100% rename from src/containers/Projects/ProjectsOverview/index.tsx rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsOverview/index.tsx diff --git a/src/containers/Projects/ProjectsProposalSubmissionProcess/ProjectsProposalSubmissionProcess.scss b/src/containers/Projects/ProjectRulesAndGuide/ProjectsProposalSubmissionProcess/ProjectsProposalSubmissionProcess.scss similarity index 100% rename from src/containers/Projects/ProjectsProposalSubmissionProcess/ProjectsProposalSubmissionProcess.scss rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsProposalSubmissionProcess/ProjectsProposalSubmissionProcess.scss diff --git a/src/containers/Projects/ProjectsProposalSubmissionProcess/index.tsx b/src/containers/Projects/ProjectRulesAndGuide/ProjectsProposalSubmissionProcess/index.tsx similarity index 100% rename from src/containers/Projects/ProjectsProposalSubmissionProcess/index.tsx rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsProposalSubmissionProcess/index.tsx diff --git a/src/containers/Projects/ProjectsRules/ProjectsRules.scss b/src/containers/Projects/ProjectRulesAndGuide/ProjectsRules/ProjectsRules.scss similarity index 100% rename from src/containers/Projects/ProjectsRules/ProjectsRules.scss rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsRules/ProjectsRules.scss diff --git a/src/containers/Projects/ProjectsRules/index.tsx b/src/containers/Projects/ProjectRulesAndGuide/ProjectsRules/index.tsx similarity index 100% rename from src/containers/Projects/ProjectsRules/index.tsx rename to src/containers/Projects/ProjectRulesAndGuide/ProjectsRules/index.tsx diff --git a/src/containers/Projects/ProjectRulesAndGuide/index.tsx b/src/containers/Projects/ProjectRulesAndGuide/index.tsx new file mode 100644 index 000000000..738d1ef37 --- /dev/null +++ b/src/containers/Projects/ProjectRulesAndGuide/index.tsx @@ -0,0 +1,53 @@ +import React, {FC, useMemo} from 'react'; +import {Redirect, useParams} from 'react-router-dom'; + +import {DashboardLayout, Pagination} from 'components'; +import {PageData, PageDataObject} from 'types/page-data'; + +import ProjectsMenuItems, {projectsNavigationData} from '../ProjectsMenuItems'; +import ProjectsMilestones from './ProjectsMilestones'; +import ProjectsOverview from './ProjectsOverview'; +import ProjectsProposalSubmissionProcess from './ProjectsProposalSubmissionProcess'; +import ProjectsRules from './ProjectsRules'; + +const defaultPageData: PageData = { + content: , + name: '', +}; + +const pageData: PageDataObject = { + milestones: { + content: , + name: 'Milestones & Payouts', + }, + overview: { + content: , + name: 'Overview', + }, + proposals: { + content: , + name: 'Proposal Submission Process', + }, + rules: { + content: , + name: 'Rules & Guidelines', + }, +}; + +const getPageData = (chapter: string): PageData => { + return pageData[chapter] || defaultPageData; +}; + +const Projects: FC = () => { + const {chapter} = useParams<{chapter: string}>(); + const {content, name} = useMemo(() => getPageData(chapter), [chapter]); + + return ( + } pageName={name} sectionName="Projects"> + {content} + + + ); +}; + +export default Projects; diff --git a/src/containers/Projects/Projects.scss b/src/containers/Projects/Projects.scss new file mode 100644 index 000000000..2dcfc349d --- /dev/null +++ b/src/containers/Projects/Projects.scss @@ -0,0 +1,11 @@ +.Projects { + overflow-x: hidden; + + &__loading-container { + align-items: center; + display: flex; + height: 80vh; + justify-content: center; + width: 100%; + } +} diff --git a/src/containers/Projects/ProjectsHero/ProjectsHero.scss b/src/containers/Projects/ProjectsHero/ProjectsHero.scss new file mode 100644 index 000000000..7b7590ca5 --- /dev/null +++ b/src/containers/Projects/ProjectsHero/ProjectsHero.scss @@ -0,0 +1,109 @@ +@import 'styles/font-mixins'; + +$gap-width: 50px; +$left-pl: 64px; +$right-width: 714px; +$title-width: 480px; + +.ProjectsHero { + display: flex; + height: 524px; + justify-content: center; + padding-top: 33px; + position: relative; + + @media (max-width: 1200px) { + align-items: center; + flex-direction: column; + height: unset; + padding-bottom: 60px; + } + + &__cta-container { + display: flex; + flex-direction: row; + } + + &__first-button { + margin-right: 16px; + } + + &__image { + max-width: 695px; + width: 100%; + } + + &__left { + align-items: center; + display: flex; + justify-content: center; + margin-right: $gap-width; + padding: 128px 0 128px $left-pl; + + @media (max-width: 768px) { + margin-right: 0; + max-width: 100vw; + padding: 64px 32px 0 32px; + } + } + + &__left-content-container { + @media (max-width: 768px) { + align-items: center; + display: flex; + flex-direction: column; + text-align: center; + } + } + + &__right { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + max-width: $right-width; + position: relative; + width: $right-width; + + @media (max-width: 1200px) { + max-width: 450px; + } + + @media (max-width: 768px) { + margin: 60px 12px 0; + max-width: 100vw; + text-align: center; + } + } + + &__subtitle, + &__title { + max-width: $title-width; + } + + &__subtitle { + @extend %h3; + color: var(--color-gray-500); + font-weight: var(--font-weight-regular); + line-height: 1.5; + margin-bottom: 40px; + margin-top: 30px; + } + + &__title { + @extend %display1; + font-weight: var(--font-weight-bold); + line-height: 1.25; + } + + &__wrapper { + align-items: center; + display: flex; + max-width: $title-width + $gap-width + $right-width + $left-pl; + + @media (max-width: 768px) { + align-items: inherit; + flex-direction: column; + } + } +} diff --git a/src/containers/Projects/ProjectsHero/ProjectsHeroImage.tsx b/src/containers/Projects/ProjectsHero/ProjectsHeroImage.tsx new file mode 100644 index 000000000..b70837e5f --- /dev/null +++ b/src/containers/Projects/ProjectsHero/ProjectsHeroImage.tsx @@ -0,0 +1,386 @@ +import React, {FC} from 'react'; + +const ProjectsHeroImage: FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectsHeroImage; diff --git a/src/containers/Projects/ProjectsHero/index.tsx b/src/containers/Projects/ProjectsHero/index.tsx new file mode 100644 index 000000000..e92b914fa --- /dev/null +++ b/src/containers/Projects/ProjectsHero/index.tsx @@ -0,0 +1,47 @@ +import React, {FC} from 'react'; +import {useHistory} from 'react-router-dom'; + +import {Button} from 'components'; +import ProjectsHeroImage from './ProjectsHeroImage'; +import './ProjectsHero.scss'; + +const ProjectsHero: FC = () => { + const history = useHistory(); + + return ( +
+
+
+
+

Projects

+

+ Earn coins by building apps, games, tools, and other software for thenewboston network. +

+
+ + +
+
+
+
+ +
+
+
+ ); +}; + +export default ProjectsHero; diff --git a/src/components/ProjectsMenuItems/ProjectsMenuItems.test.tsx b/src/containers/Projects/ProjectsMenuItems/ProjectsMenuItems.test.tsx similarity index 100% rename from src/components/ProjectsMenuItems/ProjectsMenuItems.test.tsx rename to src/containers/Projects/ProjectsMenuItems/ProjectsMenuItems.test.tsx diff --git a/src/components/ProjectsMenuItems/index.tsx b/src/containers/Projects/ProjectsMenuItems/index.tsx similarity index 78% rename from src/components/ProjectsMenuItems/index.tsx rename to src/containers/Projects/ProjectsMenuItems/index.tsx index cd2ec09f2..5a7b055d4 100644 --- a/src/components/ProjectsMenuItems/index.tsx +++ b/src/containers/Projects/ProjectsMenuItems/index.tsx @@ -7,19 +7,19 @@ import {NavigationItem} from 'types/navigation'; export const projectsNavigationData = [ { name: 'Overview', - url: '/projects/overview', + url: '/project-rules/overview', }, { name: 'Rules & Guidelines', - url: '/projects/rules', + url: '/project-rules/rules', }, { name: 'Proposal Submission Process', - url: '/projects/proposals', + url: '/project-rules/proposals', }, { name: 'Milestones & Payouts', - url: '/projects/milestones', + url: '/project-rules/milestones', }, ]; @@ -33,7 +33,7 @@ const ProjectsMenuItems: FC = () => { }; return ( - + {renderNavLinks(projectsNavigationData)} ); diff --git a/src/containers/Projects/_shared.scss b/src/containers/Projects/_shared.scss new file mode 100644 index 000000000..8f0945765 --- /dev/null +++ b/src/containers/Projects/_shared.scss @@ -0,0 +1,20 @@ +$top-nav-height: 60px; +$details-header-height: 158px; +$details-header-height-480: 241px; +$top-content-height: calc(#{$top-nav-height} + #{$details-header-height}); +$top-content-height-480: calc(#{$top-nav-height} + #{$details-header-height-480}); + +// cards +$card-width-original: 384px; +$card-min-width-original: 300px; +$card-height-original: 324px; + +$card-width-992: 432px; +$card-min-width-992: 330px; + +$card-width-768: 320px; +$card-min-width-768: 220px; +$card-height-768: 262px; + +$card-height-480: 324px; +$card-width-480: 376px; diff --git a/src/containers/Projects/index.tsx b/src/containers/Projects/index.tsx index 62ab9264a..01a441e6c 100644 --- a/src/containers/Projects/index.tsx +++ b/src/containers/Projects/index.tsx @@ -1,53 +1,66 @@ -import React, {FC, useMemo} from 'react'; +import React, {FC, useEffect, useMemo, useState} from 'react'; import {Redirect, useParams} from 'react-router-dom'; -import {DashboardLayout, ProjectsMenuItems, Pagination} from 'components'; -import {projectsNavigationData} from 'components/ProjectsMenuItems'; -import {PageData, PageDataObject} from 'types/page-data'; +import {api as projectsApi} from 'apis/projects'; +import {Project} from 'types/projects'; +import {Loader} from 'components'; +import ListOfProjects from './ListOfProjects'; +import ProjectsHero from './ProjectsHero'; +import ProjectDetails from './ProjectDetails'; -import ProjectsMilestones from './ProjectsMilestones'; -import ProjectsOverview from './ProjectsOverview'; -import ProjectsProposalSubmissionProcess from './ProjectsProposalSubmissionProcess'; -import ProjectsRules from './ProjectsRules'; +import './Projects.scss'; -const defaultPageData: PageData = { - content: , - name: '', -}; - -const pageData: PageDataObject = { - milestones: { - content: , - name: 'Milestones & Payouts', - }, - overview: { - content: , - name: 'Overview', - }, - proposals: { - content: , - name: 'Proposal Submission Process', - }, - rules: { - content: , - name: 'Rules & Guidelines', - }, -}; +const Projects: FC = () => { + const {projectId} = useParams<{projectId: string}>(); + const [projects, setProjects] = useState([]); + const [isLoading, setIsLoading] = useState(true); -const getPageData = (chapter: string): PageData => { - return pageData[chapter] || defaultPageData; -}; + const isValidProjectId = useMemo( + () => !!(projectId && projects.length && projects.some((project) => project.pk === projectId)), + [projectId, projects], + ); -const Projects: FC = () => { - const {chapter} = useParams<{chapter: string}>(); - const {content, name} = useMemo(() => getPageData(chapter), [chapter]); - - return ( - } pageName={name} sectionName="Projects"> - {content} - - + const selectedProject = useMemo( + () => (isValidProjectId ? projects.find((project) => project.pk === projectId) || null : null), + [isValidProjectId, projects, projectId], ); + + useEffect(() => { + (async function getProjects() { + try { + const projectsResponse = await projectsApi.getProjects(); + const sortedProjects = projectsResponse.sort((a, b) => (a.title > b.title ? 1 : -1)); + setProjects(sortedProjects); + } catch (err) { + // handle error + } finally { + setIsLoading(false); + } + })(); + }, []); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!projectId) { + return ( +
+ + +
+ ); + } + + if (selectedProject) { + return ; + } + + return ; }; export default Projects; diff --git a/src/containers/Teams/TeamTabs/index.tsx b/src/containers/Teams/TeamTabs/index.tsx index abbee5024..380bd6913 100644 --- a/src/containers/Teams/TeamTabs/index.tsx +++ b/src/containers/Teams/TeamTabs/index.tsx @@ -1,5 +1,5 @@ import React, {FC, ReactNode, useEffect, useMemo, useState} from 'react'; -import {useHistory} from 'react-router'; +import {useHistory} from 'react-router-dom'; import clsx from 'clsx'; import './TeamTabs.scss'; diff --git a/src/containers/TopNav/TopNav.scss b/src/containers/TopNav/TopNav.scss index 5cfb011da..d4f0816ce 100644 --- a/src/containers/TopNav/TopNav.scss +++ b/src/containers/TopNav/TopNav.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + $dropdown-height: 316px; $mobile-dropdown-height: 1000px; $top-nav-height: 60px; @@ -31,7 +33,7 @@ $top-nav-height: 60px; flex: 1; height: $top-nav-height; position: relative; - z-index: 3; + z-index: $z-index-top-nav; } &__right { diff --git a/src/containers/TopNav/TopNavDesktopItems/index.tsx b/src/containers/TopNav/TopNavDesktopItems/index.tsx index 70df9441b..1d5fa4260 100644 --- a/src/containers/TopNav/TopNavDesktopItems/index.tsx +++ b/src/containers/TopNav/TopNavDesktopItems/index.tsx @@ -71,7 +71,7 @@ const morePopoverItems: TopNavPopoverItemType[] = [ description: 'Propose ideas you want built', iconType: IconType.hammerWrench, title: 'Projects', - to: '/projects/overview', + to: '/projects', }, { description: 'Download thenewboston assets', diff --git a/src/containers/TopNav/TopNavMobileMenu/TopNavMobileMenu.scss b/src/containers/TopNav/TopNavMobileMenu/TopNavMobileMenu.scss index 931d79dd9..aac991296 100644 --- a/src/containers/TopNav/TopNavMobileMenu/TopNavMobileMenu.scss +++ b/src/containers/TopNav/TopNavMobileMenu/TopNavMobileMenu.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + $dropdown-height: 316px; $mobile-dropdown-height: 1000px; $top-nav-height: 60px; @@ -57,7 +59,7 @@ $top-nav-height: 60px; position: relative; transition: background 0.1s ease-in; width: $top-nav-height; - z-index: 3; + z-index: $z-index-top-nav-mobile-button; @media (hover: hover) and (pointer: fine) { &:hover { @@ -109,7 +111,7 @@ $top-nav-height: 60px; position: fixed; top: $top-nav-height; width: 100%; - z-index: 2; + z-index: $z-index-top-nav-mobile-dropdown; @media (max-width: 992px) { animation: mobileSlideDown 0.3s forwards; @@ -153,7 +155,7 @@ $top-nav-height: 60px; position: fixed; top: $top-nav-height; width: 100%; - z-index: 1; + z-index: $z-index-top-nav-mobile-overlay; } &__separator { diff --git a/src/containers/TopNav/TopNavMobileMenu/index.tsx b/src/containers/TopNav/TopNavMobileMenu/index.tsx index 9f2af1126..8436a073c 100644 --- a/src/containers/TopNav/TopNavMobileMenu/index.tsx +++ b/src/containers/TopNav/TopNavMobileMenu/index.tsx @@ -105,7 +105,7 @@ const TopNavMobileMenu: FC = ({closeMenu, menuOpen, smallDevice, 'more', 'More', <> - {renderMobileLink('Projects', '/projects/overview')} + {renderMobileLink('Projects', '/projects')} {renderMobileLink('Blog', 'https://thenewboston.blog/', true)} {renderMobileLink('Assets', '/assets')} {renderMobileLink('FAQ', '/faq')} diff --git a/src/containers/TopNav/TopNavPopover/index.tsx b/src/containers/TopNav/TopNavPopover/index.tsx index dbde52ab4..56f164fff 100644 --- a/src/containers/TopNav/TopNavPopover/index.tsx +++ b/src/containers/TopNav/TopNavPopover/index.tsx @@ -1,5 +1,5 @@ import React, {FC, KeyboardEvent, ReactNode, useCallback, useEffect, useRef} from 'react'; -import {useHistory} from 'react-router'; +import {useHistory} from 'react-router-dom'; import clsx from 'clsx'; import {Icon, IconType} from '@thenewboston/ui'; diff --git a/src/containers/Tutorials/index.tsx b/src/containers/Tutorials/index.tsx index d6b278c5d..5a5365848 100644 --- a/src/containers/Tutorials/index.tsx +++ b/src/containers/Tutorials/index.tsx @@ -1,5 +1,5 @@ import React, {FC, ReactNode, useCallback, useEffect, useState} from 'react'; -import {useHistory, useParams} from 'react-router'; +import {useHistory, useParams} from 'react-router-dom'; import {getPlaylistCategories} from 'apis/tutorials'; import {BreadcrumbMenu, FlatNavLinks, Loader, PageTitle} from 'components'; diff --git a/src/containers/Webmap/Webmap.scss b/src/containers/Webmap/Webmap.scss index 4b8829073..9c67650e2 100644 --- a/src/containers/Webmap/Webmap.scss +++ b/src/containers/Webmap/Webmap.scss @@ -1,11 +1,13 @@ @import '~leaflet/dist/leaflet.css'; +@import 'styles/z-index'; + $map-bottom-offset: 200px; .Map { height: calc(100vh - #{$map-bottom-offset}); margin-top: 10px; width: 100%; - z-index: 0; + z-index: $z-index-webmap; &__Popup { &-heading { diff --git a/src/styles/_colors.scss b/src/styles/_colors.scss index b6c1bee23..02872f903 100644 --- a/src/styles/_colors.scss +++ b/src/styles/_colors.scss @@ -2,6 +2,8 @@ --color-alert: #ed5f74; --color-bg: #fff; --color-black: black; + --color-blue-50: #f5fbff; + --color-blue-900: #131f41; --color-blue-gray-1000: #212d63; --color-blue-gray-100: #cfd8dc; --color-blue-gray-200: #b0bec5; @@ -13,6 +15,7 @@ --color-blue-gray-700: #455a64; --color-blue-gray-800: #37474f; --color-blue-gray-900: #263238; + --color-cyan-800: #093353; --color-danger: #d30c15; --color-github-blue: #0366d6; --color-gray-050: #f6f9fc; @@ -20,6 +23,7 @@ --color-gray-100: #e3e8ee; --color-gray-1: #333333; --color-gray-2: #4f4f4f; + --color-gray-200: #c1c9d2; --color-gray-300: #a3acb9; --color-gray-3: #828282; --color-gray-4: #e0e0e0; @@ -54,6 +58,7 @@ --color-sail-gray-400: #8792a2; --color-sail-gray-500: #697386; --color-sail-gray-50: #f7fafc; + --color-sail-gray-600: #4f566b; --color-sail-gray-700: #3c4257; --color-sail-gray-900: #042235; --color-sail-green-500: #09825d; diff --git a/src/styles/_core.scss b/src/styles/_core.scss index ea320f132..580ae3a0f 100644 --- a/src/styles/_core.scss +++ b/src/styles/_core.scss @@ -1,3 +1,5 @@ +@import 'styles/z-index'; + * { box-sizing: border-box; } @@ -17,13 +19,13 @@ body { #popover-root { position: fixed; top: 0; - z-index: 200; + z-index: $z-index-popover-root; } #slide-up-root { bottom: 0; position: fixed; - z-index: 150; + z-index: $z-index-slideup-root; } b, diff --git a/src/styles/_z-index.scss b/src/styles/_z-index.scss new file mode 100644 index 000000000..a355d2d8a --- /dev/null +++ b/src/styles/_z-index.scss @@ -0,0 +1,20 @@ +$z-index-webmap: 0; + +$z-index-project-header: 1; + +$z-index-breadcrumb: 2; +$z-index-go-to-top: 2; +$z-index-layout: 2; +$z-index-modal: 2; +$z-index-slideup-overlay: 2; +$z-index-slideup: 2; +$z-index-top-nav-mobile-overlay: 2; + +$z-index-top-nav-mobile-dropdown: 3; + +$z-index-top-nav: 4; +$z-index-top-nav-mobile-button: 4; + +$z-index-slideup-root: 150; + +$z-index-popover-root: 200; diff --git a/src/types/api.ts b/src/types/api.ts index 83b608d6f..a0707202e 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -11,3 +11,10 @@ export interface AuthResponse { export interface Token { token: string; } + +export interface PaginatedResponse { + count: number; + next: number | null; + previous: number | null; + results: T[]; +} diff --git a/src/types/projects.ts b/src/types/projects.ts new file mode 100644 index 000000000..c79882d2b --- /dev/null +++ b/src/types/projects.ts @@ -0,0 +1,61 @@ +import {ProjectIconType} from 'containers/Projects/ProjectIcons'; + +export type Project = { + pk: string; + created_date: string; + modified_date: string; + title: string; + description: string; + logo: string; + github_url: string; + overview: string; + problem: string; + target_market: string; + benefits: string; + centered_around_tnb: string; + estimated_completion_date: string; + project_lead: string; + project_lead_display_name: string; + milestones: Milestone[]; +}; + +export type Milestone = { + uuid: string; + created_date: string; + modified_date: string; + number: number; + description: string; + project: string; +}; + +export type ProjectTopicMap = { + [key: string]: ProjectTopic; +}; + +export type ProjectTopic = { + anchor: ProjectTopicAnchor; + iconType: ProjectIconType; + overview: string; + position: number; + title: ProjectTopicTitle; +}; + +export enum ProjectTopicTitle { + Overview = 'Overview', + Problem = 'Problem', + TargetMarket = 'Target Market', + Benefits = 'Benefit to Network & Community', + CenteredAroundTNB = 'Centered around TNB', + EstimatedCompletionDate = 'Estimated completion date', + Roadmap = 'Roadmap', +} + +export enum ProjectTopicAnchor { + Overview = 'topic-overview', + Problem = 'topic-problem', + TargetMarket = 'topic-target-market', + Benefits = 'topic-benefits', + CenteredAroundTNB = 'topic-centered-around-tnb', + EstimatedCompletionDate = 'topic-completion-date', + Roadmap = 'topic-roadmap', +} diff --git a/src/utils/device.ts b/src/utils/device.ts new file mode 100644 index 000000000..aced98607 --- /dev/null +++ b/src/utils/device.ts @@ -0,0 +1,3 @@ +export const isTouchScreenDevice = () => { + return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; +};