24
loading...
This website collects cookies to deliver better user experience
create-react-app
.npx create-react-app roadmap
Roadmap
- that'll contain our display and API logic - src/components/Roadmap
.Roadmap.js
, index.js
and Roadmap.css
files. Add following code in respective order.import React from "react";
import "./Roadmap.css";
function Roadmap() {
const columnMap = [
{
title: "\"Exploring\","
tickets: [],
},
{
title: "\"In Progress\","
tickets: [],
},
{
title: "\"Done\","
tickets: [],
},
{
title: "\"Leaving it for now\","
tickets: [],
},
];
return (
<div className="roadmap">
{columnMap.map((column, i) => (
<div className="roadmap-column" key={`${column.title}-${i}`}>
<div className="roadmap-column-heading">{column.title}</div>
<div className="roadmap-cards">
{column.tickets.map((t, i) => (
<div key={`exploring-${i}`} className="roadmap-cards-item">
<div className="roadmap-cards-item-heading">{t.title}</div>
<div
className="roadmap-cards-item-content"
dangerouslySetInnerHTML={{ __html: t.description }}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 100 125"
style={{
enableBackground: "new 0 0 100 100",
maxWidth: "18px",
cursor: "pointer",
}}
>
<g>
<g>
<path d="M81.7,40.4H64.6l3.6-17.9c0.5-2.5,0.1-5-1.2-7.1l0,0c-1.5-2.5-4.9-3-7-1.1L29.8,38.7c-1.2,1.1-1.8,2.6-1.8,4.1v33.1 c0,2.4,1.5,4.5,3.7,5.3l12.9,3.4c3.1,1.1,6.4,1.6,9.7,1.6h18c6.6,0,12.5-4.2,14.7-10.5l6.9-20c0.3-0.9,0.4-1.9,0.4-2.9v0 C94.3,46,88.7,40.4,81.7,40.4z" />
<path d="M22.2,40.4H7.8c-1.5,0-2.8,1.2-2.8,2.8v34c0,1.5,1.2,2.7,2.7,2.8h14.5c1.5,0,2.8-1.2,2.8-2.8v-34 C25,41.6,23.7,40.4,22.2,40.4z" />
</g>
</g>
</svg>
</div>
))}
</div>
</div>
))}
</div>
);
}
export default Roadmap;
export { default } from "./Roadmap";
.roadmap {
display: flex;
width: 100%;
max-width: 900px;
min-height: 400px;
}
.roadmap > div:not(:last-child) {
margin-right: 16px;
}
.roadmap-column {
display: flex;
flex-direction: column;
flex: 1;
background: #eef0fc;
border-radius: 4px;
padding: 6px 10px;
}
.roadmap-column-heading {
color: #4d5273;
font-size: 14px;
font-weight: 500;
padding: 10px;
margin-bottom: 10px;
text-align: center;
}
.roadmap-cards {
display: flex;
flex-direction: column;
}
.roadmap-cards > div:not(:last-child) {
margin-bottom: 12px;
}
.roadmap-cards-item {
background: #fff;
border-radius: 4px;
padding: 12px;
box-shadow: 0 1px 0 #091e4240;
}
.roadmap-cards-item-heading {
font-size: 14px;
font-weight: 500;
}
.roadmap-cards-item-upvote {
display: flex;
font-size: 12px;
line-height: 18px;
}
.roadmap-cards-item-upvote-count {
margin-left: 6px;
}
.roadmap-cards-item p {
font-size: 12px;
margin-top: 7px;
line-height: 1.5;
}
svg.not-filled path {
fill: #fff;
stroke: #000;
stroke-width: 8px;
}
@media (max-width: 600px) {
.roadmap {
flex-direction: column;
}
.roadmap > div:not(:last-child) {
margin-right: 0;
margin-bottom: 26px;
}
}
App.js
and see how our component looks.App.js
and remove all the unnecessary files and code. Our code - App.js
, App.css
and component should look like below.App.js
and App.css
respectively.import React from "react";
import Roadmap from "./components/Roadmap";
import "./App.css";
function App() {
return (
<div>
<div className="wrapper-heading">Roadmap</div>
<Roadmap />
</div>
);
}
export default App;
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap");
html,
body {
font-family: "Roboto", sans-serif;
}
.wrapper-heading {
padding: 16px;
text-align: center;
margin-bottom: 18px;
max-width: 900px;
color: #4d5273;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.6px;
}
yarn start
in root of our project and our component should look like this.Deploy and get the backend URL - Having cloned the project, it's now time to deploy it so that we can get our APIs and backend hosted. Open the cloned project if you are not already there and you’ll see a Deploy
button on the top right.
Click Deploy
→ Select an environment → Hit deploy.
Upon completing the project, it will provide an API URL.
yarn add @apollo/client graphql
App.js
so it would communicate with the backend.uri
with the one you'll get from Canonic.import React from "react";
import { ApolloProvider, InMemoryCache, ApolloClient } from "@apollo/client";
import Roadmap from "./components/Roadmap";
import "./App.css";
/**
* Connecting with the backend using apollo client
*/
const client = new ApolloClient({
// make sure you update the URI here to point to your backend
uri: "https://roadmap-35a418.can.canonic.dev/graphql",
cache: new InMemoryCache(),
});
function App() {
return (
<ApolloProvider client={client}>
<div>
<div className="wrapper-heading">Roadmap</div>
<Roadmap />
</div>
</ApolloProvider>
);
}
export default App;
src/gql
. Inside which we create a file and name it queries.js
. import { gql } from "@apollo/client";
/**
* gql query to get roadmap
* The query parameters we got straight from Canonic autogenerated documentation
*/
export const GET_ROADMAP = gql`
query {
roadmaps {
_id
title
description
upvotes {
count
}
stage {
value
}
}
}
`;
Roadmap.js
to achieve the above mentioned logic and get the tickets.import React from "react";
import { useQuery } from "@apollo/client";
import { GET_ROADMAP } from "../../gql/queries";
import "./Roadmap.css";
function Roadmap() {
const { data = {}, loading } = useQuery(GET_ROADMAP);
const { roadmaps = [] } = data;
// dividing tickets into their respective categories
const exploringTickets = roadmaps.filter(
(t) => t.stage.value === "EXPLORING"
);
const inProgressTickets = roadmaps.filter(
(t) => t.stage.value === "IN_PROGRESS"
);
const doneTickets = roadmaps.filter((t) => t.stage.value === "DONE");
const leavingItForNowTickets = roadmaps.filter(
(t) => t.stage.value === "LEAVING_IT_FOR_NOW"
);
const columnMap = [
{
title: "Exploring",
tickets: exploringTickets,
},
{
title: "In Progress",
tickets: inProgressTickets,
},
{
title: "Done",
tickets: doneTickets,
},
{
title: "Leaving it for now",
tickets: leavingItForNowTickets,
},
];
return (
<div className="roadmap">
{loading ? (
<svg
xmlns="http://www.w3.org/2000/svg"
style={{
margin: "auto",
background: "none",
display: "block",
shapeRendering: "auto",
maxWidth: "30px",
marginTop: "-20px",
}}
width="200px"
height="200px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<circle
cx="50"
cy="50"
fill="none"
stroke="#4d5273"
strokeWidth="10"
r="35"
strokeDasharray="164.93361431346415 56.97787143782138"
>
<animateTransform
attributeName="transform"
type="rotate"
repeatCount="indefinite"
dur="1s"
values="0 50 50;360 50 50"
keyTimes="0;1"
></animateTransform>
</circle>
</svg>
) : (
<>
{columnMap.map((column, i) => (
<div className="roadmap-column" key={`${column.title}-${i}`}>
<div className="roadmap-column-heading">{column.title}</div>
<div className="roadmap-cards">
{column.tickets.map((t, i) => (
<div key={`exploring-${i}`} className="roadmap-cards-item">
<div className="roadmap-cards-item-heading">{t.title}</div>
<div
className="roadmap-cards-item-content"
dangerouslySetInnerHTML={{ __html: t.description }}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 100 125"
style={{
enableBackground: "new 0 0 100 100",
maxWidth: "18px",
cursor: "pointer",
}}
>
<g>
<g>
<path d="M81.7,40.4H64.6l3.6-17.9c0.5-2.5,0.1-5-1.2-7.1l0,0c-1.5-2.5-4.9-3-7-1.1L29.8,38.7c-1.2,1.1-1.8,2.6-1.8,4.1v33.1 c0,2.4,1.5,4.5,3.7,5.3l12.9,3.4c3.1,1.1,6.4,1.6,9.7,1.6h18c6.6,0,12.5-4.2,14.7-10.5l6.9-20c0.3-0.9,0.4-1.9,0.4-2.9v0 C94.3,46,88.7,40.4,81.7,40.4z" />
<path d="M22.2,40.4H7.8c-1.5,0-2.8,1.2-2.8,2.8v34c0,1.5,1.2,2.7,2.7,2.8h14.5c1.5,0,2.8-1.2,2.8-2.8v-34 C25,41.6,23.7,40.4,22.2,40.4z" />
</g>
</g>
</svg>
</div>
))}
</div>
</div>
))}
</>
)}
</div>
);
}
export default Roadmap;
src/gql/mutations.js
. Taking a reference from auto-generated documentation, we can specify the mutation parameters.import { gql } from "@apollo/client";
/**
* gql query to get roadmap
* The query parameters we got straight from Canonic autogenerated documentation
*/
export const UPVOTE = gql`
mutation Upvote($ticketId: ID!) {
createUpvote(input: { ticket: $ticketId }) {
_id
}
}
`;
Roadmap
component. In this case, we will save the upvote not only on the backend but also in the local storage since we will not be creating a user on the backend and we want to retain the upvote information.Roadmap.js
and add the mutation logic. Our final file looks like below.import React from "react";
import { useQuery, useMutation } from "@apollo/client";
import { GET_ROADMAP } from "../../gql/queries";
import { UPVOTE } from "../../gql/mutations";
import "./Roadmap.css";
function Roadmap() {
const { data = {}, loading } = useQuery(GET_ROADMAP);
const [upvoteTicket] = useMutation(UPVOTE, {
context: {
headers: {
Authorization:
"617bdcfc530d0d0009c04985-c2ca6caf-485c-4bc1-8ac8-4b9defe2707e",
},
},
});
const { roadmaps = [] } = data;
const [upvotes, setUpvotes] = React.useState([]);
// dividing tickets into their respective categories
const exploringTickets = roadmaps.filter(
(t) => t.stage.value === "EXPLORING"
);
const inProgressTickets = roadmaps.filter(
(t) => t.stage.value === "IN_PROGRESS"
);
const doneTickets = roadmaps.filter((t) => t.stage.value === "DONE");
const leavingItForNowTickets = roadmaps.filter(
(t) => t.stage.value === "LEAVING_IT_FOR_NOW"
);
const columnMap = [
{
title: "Exploring",
tickets: exploringTickets,
},
{
title: "In Progress",
tickets: inProgressTickets,
},
{
title: "Done",
tickets: doneTickets,
},
{
title: "Leaving it for now",
tickets: leavingItForNowTickets,
},
];
const handleUpvoteTicket = React.useCallback(
async (id) => {
setUpvotes((upvotes) => [...upvotes, id]);
localStorage.setItem(`${id}`, true);
upvoteTicket({ variables: { ticketId: id } });
},
[upvoteTicket]
);
const isTicketUpvoted = React.useCallback(
(id) => !!upvotes.find((t) => t === id) || localStorage.getItem(id),
[upvotes]
);
return (
<div className="roadmap">
{loading ? (
<svg
xmlns="http://www.w3.org/2000/svg"
style={{
margin: "auto",
background: "none",
display: "block",
shapeRendering: "auto",
maxWidth: "30px",
marginTop: "-20px",
}}
width="200px"
height="200px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<circle
cx="50"
cy="50"
fill="none"
stroke="#4d5273"
strokeWidth="10"
r="35"
strokeDasharray="164.93361431346415 56.97787143782138"
>
<animateTransform
attributeName="transform"
type="rotate"
repeatCount="indefinite"
dur="1s"
values="0 50 50;360 50 50"
keyTimes="0;1"
></animateTransform>
</circle>
</svg>
) : (
<>
{columnMap.map((column, i) => (
<div className="roadmap-column" key={`${column.title}-${i}`}>
<div className="roadmap-column-heading">{column.title}</div>
<div className="roadmap-cards">
{column.tickets.map((t, i) => (
<div key={`exploring-${i}`} className="roadmap-cards-item">
<div className="roadmap-cards-item-heading">{t.title}</div>
<div
className="roadmap-cards-item-content"
dangerouslySetInnerHTML={{ __html: t.description }}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 100 125"
style={{
enableBackground: "new 0 0 100 100",
maxWidth: "18px",
cursor: "pointer",
}}
className={!isTicketUpvoted(t._id) && "not-filled"}
onClick={() => handleUpvoteTicket(t._id)}
>
<g>
<g>
<path d="M81.7,40.4H64.6l3.6-17.9c0.5-2.5,0.1-5-1.2-7.1l0,0c-1.5-2.5-4.9-3-7-1.1L29.8,38.7c-1.2,1.1-1.8,2.6-1.8,4.1v33.1 c0,2.4,1.5,4.5,3.7,5.3l12.9,3.4c3.1,1.1,6.4,1.6,9.7,1.6h18c6.6,0,12.5-4.2,14.7-10.5l6.9-20c0.3-0.9,0.4-1.9,0.4-2.9v0 C94.3,46,88.7,40.4,81.7,40.4z" />
<path d="M22.2,40.4H7.8c-1.5,0-2.8,1.2-2.8,2.8v34c0,1.5,1.2,2.7,2.7,2.8h14.5c1.5,0,2.8-1.2,2.8-2.8v-34 C25,41.6,23.7,40.4,22.2,40.4z" />
</g>
</g>
</svg>
</div>
))}
</div>
</div>
))}
</>
)}
</div>
);
}
export default Roadmap;