Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
import React from "react";
import "./src/styles/scss/index.scss";

import { UserProvider } from "./src/context/UserContext";
export const wrapRootElement = ({ element }) => (
<UserProvider>{element}</UserProvider>
);
2 changes: 1 addition & 1 deletion gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const path = require("path");
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;

const ProductPage = path.resolve("./src/pages/book/_slug.tsx");
const ProductPage = path.resolve("./src/components/Product.tsx");

const result = await graphql(
`
Expand Down
16 changes: 10 additions & 6 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { Link, navigate } from "gatsby";
import UserContext from "../context/UserContext";

type NavbarProps = {
showBackButton?: boolean;
};

const Navbar = ({ showBackButton }: NavbarProps) => {
const [token] = useState("");
const { user, setUser } = useContext(UserContext);
const [loading, setLoading] = useState(false);

const handleSignOut = async (event: any) => {
event.preventDefault();
setLoading(true);
try {
await fetch(`/api/logout?token=${token}`, {
await fetch(`/api/logout?token=${user}`, {
method: "POST",
}).catch((err) => {
console.error(`Logout error: ${err}`);
});
} finally {
window.localStorage.removeItem("google:tokens");
setLoading(false);
setUser("");
navigate("/");
}
};
Expand All @@ -37,13 +41,13 @@ const Navbar = ({ showBackButton }: NavbarProps) => {
</Link>
</li>
<li>
{!!token && (
{!!user && (
<button
className="bg-blue-400 hover:bg-blue-500 rounded-md text-white font-bold ml-auto"
type="button"
onClick={handleSignOut}
>
Log out
{loading ? "Logging out" : "Log out"}
</button>
)}
</li>
Expand Down
22 changes: 22 additions & 0 deletions src/components/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import Layout from "../components/templates/Layout";
import { navigate } from "gatsby";
import UserContext from "../context/UserContext";
import { useContext } from "react";

const PrivateRoute = ({ component: Component, ...rest }: any) => {
const { user } = useContext(UserContext);

if (!user) {
navigate(`/`);
return null;
}

return (
<Layout {...rest}>
<Component />
</Layout>
);
};

export default PrivateRoute;
File renamed without changes.
30 changes: 22 additions & 8 deletions src/pages/book/_slug.tsx → src/components/Product.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import React, { ReactPropTypes } from "react";
import React, { ReactPropTypes, useState } from "react";
import { graphql, navigate } from "gatsby";
import { get } from "lodash";
import { renderRichText } from "gatsby-source-contentful/rich-text";
import { PaystackConsumer } from "react-paystack";

import Layout from "../../components/templates/Layout";
import StarRating from "../../components/StarRating";
import StarRating from "./StarRating";

import * as styles from "./_slug.module.css";
import * as styles from "./Product.module.css";
import Layout from "./templates/Layout";

const generateReference = (): string => {
let text = "";
let possible =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

for (let i = 0; i < 10; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));

return text;
};

const payStackConfig = {
reference: new Date().getTime().toString(),
email: process.env.GATSBY_PAYSTACK_TEST_EMAIL || "",
publicKey: process.env.GATSBY_PAYSTACK_PUBLIC_KEY || "",
email: process.env.GATSBY_PAYSTACK_TEST_EMAIL,
publicKey: process.env.GATSBY_PAYSTACK_PUBLIC_KEY,
};

const ProductPage = (props: ReactPropTypes) => {
const product = get(props, `data.contentfulProduct`);
const [reference, setReference] = useState(generateReference());

const handleSuccess = (_reference: string) => {
// TODO: send reference
navigate("/book/appointment");
};

const handleClose = () => {};
const handleClose = (): void => {
setReference(generateReference());
};

const componentProps = {
reference,
...payStackConfig,
amount: product.price * 500 * 100,
text: "Book Sleep Appointment",
Expand Down
64 changes: 64 additions & 0 deletions src/components/Products.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import { graphql, Link, useStaticQuery } from "gatsby";
import { ProductType } from "../types";
import StarRating from "./StarRating";

const ProductListPage = () => {
const productsList = useStaticQuery(graphql`
query {
allContentfulProduct {
nodes {
contentful_id
name
image {
file {
fileName
url
}
}
starrating
reviews
price
}
}
}
`);

return (
<div className="container max-w-5xl bg-blue:400 mx-auto">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 mb-10">
Pick your favorite mattress
</h2>

<div className="grid grid-cols-1 gap-y-10 tablet:grid-cols-2 gap-x-6 laptop:grid-cols-3">
{productsList.allContentfulProduct.nodes.map((product: ProductType) => (
<div key={product.contentful_id}>
<div className="group relative">
<div className="group relative w-full aspect-w-1 aspect-h-1 rounded-md overflow-hidden group-hover:opacity-80 laptop:h-60 laptop:aspect-none">
<img
src={product.image.file.url}
alt=""
className="w-full h-full object-center object-cover laptop:w-full laptop:h-full"
/>
</div>
<div className="mt-4">
<Link to={`/book/${product.contentful_id}`}>
<h3 className="text-md text-gray-700 tracking-loose hover:text-blue-400 mb-1">
{product.name}
</h3>
</Link>
<p className="text-sm text-gray-900 flex items-center">
<strong> ${product.price} </strong>
<span className="mx-2">&bull;</span>
<StarRating stars={product.starrating} />
</p>
</div>
</div>
</div>
))}
</div>
</div>
);
};

export default ProductListPage;
26 changes: 12 additions & 14 deletions src/components/StarRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,18 @@ const StarRating = ({ stars, className }: StarRatingProp) => {
{[...Array(5)].map((_, index) => {
index += 1;
return (
<>
<svg
key={index}
className={`${
index <= stars ? "text-blue-400" : "text-gray-200"
} h-4 w-4 flex-shrink-0`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</>
<svg
key={index}
className={`${
index <= stars ? "text-blue-400" : "text-gray-200"
} h-4 w-4 flex-shrink-0`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
);
})}
</span>
Expand Down
5 changes: 2 additions & 3 deletions src/components/templates/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react";
import { Helmet } from "react-helmet";
import Navbar from "../../Navbar";
import Footer from "../../Footer";
import UserProvider from "../../../context/UserContext";

interface LayoutInterface {
children: React.ReactNode;
Expand All @@ -11,15 +10,15 @@ interface LayoutInterface {
}

const Layout = ({ children, showBackButton }: LayoutInterface) => (
<UserProvider>
<>
<Helmet>
<meta charSet="utf-8" />
<title>More sleep</title>
</Helmet>
<Navbar showBackButton={showBackButton} />
{children}
<Footer />
</UserProvider>
</>
);

export default Layout;
6 changes: 4 additions & 2 deletions src/context/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface UserProviderInterface {
children: React.ReactNode;
}

export const UserContext = createContext<UserInterface>({
const UserContext = createContext<UserInterface>({
user: "",
setUser: () => {},
});
Expand All @@ -24,4 +24,6 @@ const UserProvider = ({ children }: UserProviderInterface) => {
);
};

export default UserProvider;
export default UserContext;

export { UserProvider };
11 changes: 9 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useState } from "react";

function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
try {
if (typeof window === `undefined`) {
return;
}
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
Expand All @@ -21,7 +27,8 @@ function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val
const setValue = (value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
Expand Down
19 changes: 4 additions & 15 deletions src/middleware/private-route.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import React from "react";
import React, { useContext } from "react";
import { navigate } from "gatsby";

const isBrowser = () => typeof window !== "undefined";

const isLoggedIn = () => {
const user = getUser();
return !!user.access_token;
};

const getUser = () =>
isBrowser() && window.localStorage.getItem("google:tokens")
? JSON.parse(window.localStorage.getItem("google:tokens"))
: {};
import UserContext from "../context/UserContext";

const PrivateRoute = ({ component: Component, location, ...rest }) => {
console.log("private");
if (!isLoggedIn() && location.pathname !== `/`) {
const { user } = useContext(UserContext);
if (!user) {
navigate("/");
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { navigate } from "gatsby";
import React from "react";
import { InlineWidget, CalendlyEventListener } from "react-calendly";

import Layout from "../../components/templates/Layout";
import Layout from "../components/templates/Layout";
const BookAppointmentPage = () => {
const handleChange = (e: any) => {
const {
Expand Down
14 changes: 14 additions & 0 deletions src/pages/book.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import { Router } from "@reach/router";
import PrivateRoute from "../components/PrivateRoute";
import ProductPage from "../components/Product";
import ProductListPage from "../components/Products";

const Book = () => (
<Router>
<PrivateRoute path="/book" component={ProductListPage} />
<PrivateRoute path="/book/:slug" component={ProductPage} />
</Router>
);

export default Book;
Loading