diff --git a/data/projects/list.json b/data/projects/list.json new file mode 100644 index 000000000..86e422ec3 --- /dev/null +++ b/data/projects/list.json @@ -0,0 +1,4 @@ +{ + "items": ["Algerian_Users"], + "include": ["title"] +} diff --git a/frontend/src/apps/main/entry/index.tsx b/frontend/src/apps/main/entry/index.tsx index da012c750..a222a0999 100644 --- a/frontend/src/apps/main/entry/index.tsx +++ b/frontend/src/apps/main/entry/index.tsx @@ -13,6 +13,7 @@ import "react-toastify/dist/ReactToastify.css"; const Landing = lazy(() => import("t9/apps/main/scenes/landing")); const Articles = lazy(() => import("t9/apps/main/scenes/articles")); +const Projects = lazy(() => import("t9/apps/main/scenes/projects")); const Learn = lazy(() => import("t9/apps/main/scenes/learn")); const Contact = lazy(() => import("t9/apps/main/scenes/contact")); @@ -86,6 +87,7 @@ export const App: React.SFC<{}> = () => { + } /> diff --git a/frontend/src/apps/main/redux/actions/projects-scene/index.ts b/frontend/src/apps/main/redux/actions/projects-scene/index.ts new file mode 100644 index 000000000..36ac75fb2 --- /dev/null +++ b/frontend/src/apps/main/redux/actions/projects-scene/index.ts @@ -0,0 +1,64 @@ +import { actionType } from "../../constants"; +import { Dispatch } from "react"; +import { MainStoreStateInterface } from "t9/types/main"; +import Axios from "axios"; +import { hasInCollection } from "src/common/utils"; +import { Project } from "t9/types/fullstack"; +import { fullstackConfig } from "src/config"; + +const dataURL = fullstackConfig.data.url; + +export const fetchProjectsList = () => async ( + dispatch: Dispatch, + getState: MainStoreStateInterface, +) => { + try { + const response = await Axios.get(dataURL + "/projects/list.c.json"); + dispatch({ + type: actionType.UPDATE_PROJECTS_SCENE, + payload: { projectsList: response.data }, + }); + } catch (error) { + console.error(error); + } +}; + +export const fetchCurrentProject = () => async ( + dispatch: Dispatch, + getState: MainStoreStateInterface, +) => { + const projectSlug = location.pathname.substring( + location.pathname.indexOf("/", 1) + 1, + ); + const cashedProject = hasInCollection( + getState().projects, + "slug", + projectSlug, + [["content"]], + ); + if (cashedProject) { + // update our scene state + dispatch({ + type: actionType.UPDATE_PROJECTS_SCENE, + payload: { currentProject: cashedProject }, + }); + } else + try { + const response = await Axios.get( + dataURL + `/projects/${projectSlug}.json`, + ); + const currentProject = response.data; + // update our scene state + dispatch({ + type: actionType.UPDATE_PROJECTS_SCENE, + payload: { currentProject }, + }); + // update our cache state + dispatch({ + type: actionType.UPDATE_PROJECTS, + payload: [currentProject], + }); + } catch (error) { + console.error(error); + } +}; diff --git a/frontend/src/apps/main/redux/constants.ts b/frontend/src/apps/main/redux/constants.ts index af6521447..f04198cfc 100644 --- a/frontend/src/apps/main/redux/constants.ts +++ b/frontend/src/apps/main/redux/constants.ts @@ -3,4 +3,6 @@ export const actionType = { UPDATE_LEARN_SCENE: "UPDATE_LEARN_SCENE", UPDATE_ARTICLES: "UPDATE_ARTICLES", UPDATE_ARTICLES_SCENE: "UPDATE_ARTICLES_SCENE", + UPDATE_PROJECTS: "UPDATE_PROJECTS", + UPDATE_PROJECTS_SCENE: "UPDATE_PROJECTS_SCENE", }; diff --git a/frontend/src/apps/main/redux/reducers/index.ts b/frontend/src/apps/main/redux/reducers/index.ts index 70a45b917..b19cf8910 100644 --- a/frontend/src/apps/main/redux/reducers/index.ts +++ b/frontend/src/apps/main/redux/reducers/index.ts @@ -3,10 +3,14 @@ import { documentation } from "./documentation"; import { learnScene } from "./learn-scene"; import { articles } from "./articles"; import { articlesScene } from "./articles-scene"; +import { projects } from "./projects"; +import { projectsScene } from "./projects-scene"; export const mainReducer = combineReducers({ documentation, learnScene, articles, articlesScene, + projects, + projectsScene, }); diff --git a/frontend/src/apps/main/redux/reducers/projects-scene/index.ts b/frontend/src/apps/main/redux/reducers/projects-scene/index.ts new file mode 100644 index 000000000..9f874691c --- /dev/null +++ b/frontend/src/apps/main/redux/reducers/projects-scene/index.ts @@ -0,0 +1,20 @@ +import { ProjectsSceneProps } from "t9/apps/main/scenes/projects"; +import { actionType } from "t9/apps/main/redux/constants"; + +export const projectsScene = ( + state: ProjectsSceneProps = { + projectsList: null, + currentProject: null, + }, + action: { + type: string; + payload: ProjectsSceneProps; + }, +) => { + switch (action.type) { + case actionType.UPDATE_PROJECTS_SCENE: + return { ...state, ...action.payload }; + default: + return state; + } +}; diff --git a/frontend/src/apps/main/redux/reducers/projects/index.ts b/frontend/src/apps/main/redux/reducers/projects/index.ts new file mode 100644 index 000000000..a78fc89c5 --- /dev/null +++ b/frontend/src/apps/main/redux/reducers/projects/index.ts @@ -0,0 +1,18 @@ +import { actionType } from "t9/apps/main/redux/constants"; +import { Project } from "t9/types/fullstack"; +import { updateCollection } from "src/common/utils"; + +export const projects = ( + state: Project[] = [], + action: { + type: string; + payload: Project[]; + }, +) => { + switch (action.type) { + case actionType.UPDATE_PROJECTS: + return updateCollection(state, action.payload, "slug"); + default: + return state; + } +}; diff --git a/frontend/src/apps/main/scenes/projects/catalog/index.tsx b/frontend/src/apps/main/scenes/projects/catalog/index.tsx new file mode 100644 index 000000000..5cbff123b --- /dev/null +++ b/frontend/src/apps/main/scenes/projects/catalog/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import "./style"; +import { Project } from "t9/types/fullstack"; +import { Link } from "react-router-dom"; + +export const Catalog = (props: { projectsList: Project[] | null }) => ( +
+ {props.projectsList + ? props.projectsList.map((project, index) => ( + + {project.title} + + )) + : "Loading Projects List..."} +
+); diff --git a/frontend/src/apps/main/scenes/projects/catalog/style.scss b/frontend/src/apps/main/scenes/projects/catalog/style.scss new file mode 100644 index 000000000..d642fec2f --- /dev/null +++ b/frontend/src/apps/main/scenes/projects/catalog/style.scss @@ -0,0 +1,20 @@ +@import "../../../../../common/style/variables"; + +.projects { + .catalog { + padding: 1rem; + width: 100%; + display: inline-block; + .item { + display: block; + padding: 1rem; + cursor: pointer; + } + .item:hover { + background-color: wheat; + } + @media screen and (min-width: $regular) { + width: 20%; + } + } +} diff --git a/frontend/src/apps/main/scenes/projects/details/index.tsx b/frontend/src/apps/main/scenes/projects/details/index.tsx new file mode 100644 index 000000000..406a3ad17 --- /dev/null +++ b/frontend/src/apps/main/scenes/projects/details/index.tsx @@ -0,0 +1,80 @@ +import React, { useEffect } from "react"; +import { Project } from "t9/types/fullstack"; +import Markdown from "react-markdown"; +import "./style"; + +import programer from "t9/apps/main/assets/png/programmer.png"; +import contact from "t9/apps/main/assets/png/contact.png"; +import github from "t9/apps/main/assets/png/github.png"; +import support from "t9/apps/main/assets/png/support.png"; + +const socialMedia = [ + { + id: 1, + name: "dzcode", + href: "https://github.com/dzcode-io/dzcode.io", + icon: github, + }, + { id: 2, name: "Learn", href: "/learn", icon: programer }, + { id: 3, name: "Contact", href: "/contact", icon: contact }, + { id: 4, name: "Support", href: "/support", icon: support }, +]; + +export const Details = (props: DetailsInterface) => { + useEffect(() => { + props.fetchCurrentProject(); + setTimeout(() => { + window.FB && window.FB.XFBML.parse(); + }, 3000); + }, []); + const { currentProject } = props; + return ( +
+ {currentProject ? ( +
+ {/* Image */} + {currentProject.image && ( + {currentProject.title} + )} + {/* Title */} +

{currentProject.title}

+ {/* Description */} + {currentProject.description} +
+ {/* Details */} + +
+ {/* Contact + Edit*/} +
+ {socialMedia.map((item) => { + return ( +
+ {item.name} + {item.name} +
+ ); + })} +
+ {/* Comments */} +
+
+ ) : ( + "Loading Project..." + )} +
+ ); +}; + +export interface DetailsInterface { + fetchCurrentProject: () => void; + currentProject: Project | null; +} diff --git a/frontend/src/apps/main/scenes/projects/details/style.scss b/frontend/src/apps/main/scenes/projects/details/style.scss new file mode 100644 index 000000000..db9e88d0e --- /dev/null +++ b/frontend/src/apps/main/scenes/projects/details/style.scss @@ -0,0 +1,76 @@ +@import "../../../../../common/style/variables"; + +.projects { + .details { + display: inline-block; + vertical-align: top; + width: 100%; + align-self: center; + + @media screen and (min-width: $regular) { + width: 80%; + } + + .hero-image { + width: 100%; + max-height: 400px; + object-fit: cover; + } + + .title { + font-size: 3rem; + margin: 1rem; + } + + .description { + margin: 1rem; + } + + .break { + margin: 1rem 1rem 0; + } + + .details { + margin: 0 1rem; + width: 100%; + } + + .actions { + width: 100%; + height: 100px; + display: flex; + width: 60vw; + flex-direction: row; + align-items: center; + justify-content: space-evenly; + .item { + font-size: 1.1rem; + font-weight: 600; + display: flex; + align-items: center; + padding: 1rem; + width: 120px; + z-index: 5; + cursor: pointer; + + &:hover { + transform: scale(1.1); + } + + a { + text-decoration: none; + color: $text-primary; + } + } + + .icon { + width: calc(30px + 1vw); + height: calc(30px + 1vw); + margin-right: 20px; + } + } + + .fb-comments { + } + } +} diff --git a/frontend/src/apps/main/scenes/projects/index.tsx b/frontend/src/apps/main/scenes/projects/index.tsx new file mode 100644 index 000000000..981c90564 --- /dev/null +++ b/frontend/src/apps/main/scenes/projects/index.tsx @@ -0,0 +1,54 @@ +import { connect } from "react-redux"; +import React from "react"; +import { Catalog } from "./catalog"; +import { Details } from "./details"; +import { Project } from "t9/types/fullstack"; +import { useEffect } from "react"; +import { fetchProjectsList } from "t9/apps/main/redux/actions/projects-scene"; +import { fetchCurrentProject } from "t9/apps/main/redux/actions/projects-scene"; +import { Route, useRouteMatch } from "react-router-dom"; +import "./style"; + +export const ProjectsScene = (props: ProjectsScenePropsReduxed) => { + useEffect(() => { + props.fetchProjectsList(); + }, []); + + const { path } = useRouteMatch(); + + return ( +
+ + ( +
+ )} + /> +
+ ); +}; + +interface ProjectsScenePropsReduxed extends ProjectsSceneProps { + fetchProjectsList: () => void; + fetchCurrentProject: () => void; +} + +export interface ProjectsSceneProps { + projectsList: Project[] | null; + currentProject: Project | null; +} + +export default connect( + (state: { projectsScene: ProjectsSceneProps }) => ({ + ...state.projectsScene, + }), + (dispatch: any) => ({ + fetchProjectsList: () => dispatch(fetchProjectsList()), + fetchCurrentProject: () => dispatch(fetchCurrentProject()), + }), +)(ProjectsScene); diff --git a/frontend/src/apps/main/scenes/projects/style.scss b/frontend/src/apps/main/scenes/projects/style.scss new file mode 100644 index 000000000..8acfad9d8 --- /dev/null +++ b/frontend/src/apps/main/scenes/projects/style.scss @@ -0,0 +1,7 @@ +@import "../../../../common/style/variables"; + +.projects { + padding-top: $navbar-height; + max-width: $large; + margin: 0 auto; +} diff --git a/fullstack/src/types/index.ts b/fullstack/src/types/index.ts index 7bd85c7e7..ddad416db 100644 --- a/fullstack/src/types/index.ts +++ b/fullstack/src/types/index.ts @@ -22,4 +22,15 @@ export interface Article { views?: number; } +export interface Project { + slug: string; + image?: string; + title: string; + description?: string; + content?: string; + authors?: string[]; + contributors?: string[]; + views?: number; +} + export type Environment = "development" | "staging" | "production";