Skip to content

Commit

Permalink
Merge pull request #667 from janus-reith/storefront-v2
Browse files Browse the repository at this point in the history
Storefront v2
  • Loading branch information
willopez authored Jun 4, 2020
2 parents 1c2a7f5 + 1719683 commit d0840ee
Show file tree
Hide file tree
Showing 438 changed files with 8,043 additions and 17,737 deletions.
32 changes: 0 additions & 32 deletions .babelrc

This file was deleted.

1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CANONICAL_URL=http://localhost:4000
ENABLE_SPA_ROUTING=true
BUILD_GRAPHQL_URL=http://localhost:3000/graphql
EXTERNAL_GRAPHQL_URL=http://localhost:3000/graphql
INTERNAL_GRAPHQL_URL=http://api.reaction.localhost:3000/graphql
OAUTH2_ADMIN_PORT=4445
Expand Down
21 changes: 21 additions & 0 deletions .env.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CANONICAL_URL=http://localhost:4000
ENABLE_SPA_ROUTING=true
BUILD_GRAPHQL_URL=http://localhost:3000/graphql
EXTERNAL_GRAPHQL_URL=http://localhost:3000/graphql
INTERNAL_GRAPHQL_URL=http://api.reaction.localhost:3000/graphql
OAUTH2_ADMIN_PORT=4445
OAUTH2_ADMIN_URL=http://hydra.reaction.localhost:4445
OAUTH2_AUTH_URL=http://localhost:4444/oauth2/auth
OAUTH2_CLIENT_ID=example-storefront
OAUTH2_CLIENT_SECRET=CHANGEME
OAUTH2_PUBLIC_LOGOUT_URL=http://localhost:4444/oauth2/sessions/logout
OAUTH2_HOST=hydra.reaction.localhost
OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL=http://localhost:4100/account/change-password?email=EMAIL&from=FROM
OAUTH2_IDP_HOST_URL=http://identity.reaction.localhost:4100
OAUTH2_TOKEN_URL=http://hydra.reaction.localhost:4444/oauth2/token
PORT=4000
SEGMENT_ANALYTICS_SKIP_MINIMIZE=true
SEGMENT_ANALYTICS_WRITE_KEY=ENTER_KEY_HERE
SESSION_MAX_AGE_MS=2592000000
SESSION_SECRET=CHANGEME
STRIPE_PUBLIC_API_KEY=ENTER_STRIPE_PUBLIC_KEY_HERE
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.c9
.env*
!.env.example*
!.env.prod*
*.csv
*.dat
*.gz
Expand Down
19 changes: 8 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
FROM node:10.16.3-alpine
FROM node:12-alpine

ARG NEXTJS_DOTENV

ENV NEXTJS_DOTENV=$NEXTJS_DOTENV

# hadolint ignore=DL3018
RUN apk --no-cache add bash curl less tini vim make
Expand All @@ -12,27 +16,20 @@ RUN chown node:node .

# Copy specific things so that we can keep the image as small as possible
# without relying on each repo to include a .dockerignore file.
COPY --chown=node:node package.json ./
COPY --chown=node:node yarn.lock ./
COPY --chown=node:node ./src ./src
COPY --chown=node:node LICENSE ./

# Needed only for the build command
COPY --chown=node:node .babelrc ./
COPY --chown=node:node ./ ./

USER node

# Install ALL dependencies. We need devDependencies for the build command.
RUN yarn install --production=false --frozen-lockfile --ignore-scripts --non-interactive --no-cache

ENV BUILD_ENV=production NODE_ENV=production
RUN IS_BUILDING_NEXTJS=1 "$(npm bin)/next" build src
RUN export $(grep -v '^#' .env.${NEXTJS_DOTENV:-prod} | xargs -0) && yarn build

# Install only prod dependencies now that we've built, to make the image smaller
RUN rm -rf node_modules/*
RUN rm ./.babelrc
RUN yarn install --production=true --frozen-lockfile --ignore-scripts --non-interactive

# If any Node flags are needed, they can be set in the NODE_OPTIONS env variable.
CMD ["tini", "--", "node", "."]
CMD ["tini", "--", "yarn", "start"]
LABEL com.reactioncommerce.name="example-storefront"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ Reaction comes with a robust set of core commerce capabilities right out of the

Check out the full list of Reaction [features](https://www.reactioncommerce.com/features) and [release history](https://reactioncommerce.com/roadmap) for more info.

This example storefront is built with [Next.js](https://nextjs.org/), [React](https://reactjs.org/), [MobX](https://mobx.js.org/getting-started.html), [GraphQL](https://graphql.org/), and [Apollo Client](https://www.apollographql.com/docs/react/)
This example storefront is built with [Next.js](https://nextjs.org/), [React](https://reactjs.org/), [GraphQL](https://graphql.org/), and [Apollo Client](https://www.apollographql.com/docs/react/)

- Headless ecommerce example storefront built with [Next.js](https://nextjs.org/), [React](https://reactjs.org/), [MobX](https://mobx.js.org/getting-started.html), [GraphQL](https://graphql.org/), [Apollo Client](https://www.apollographql.com/docs/react/)
- Headless ecommerce example storefront built with [Next.js](https://nextjs.org/), [React](https://reactjs.org/), [GraphQL](https://graphql.org/), [Apollo Client](https://www.apollographql.com/docs/react/)
- [Reaction GraphQL API](https://github.com/reactioncommerce/reaction/tree/master/imports/plugins/core/graphql) integration
- Server-side rendering
- Payments with [Stripe](https://stripe.com/)
Expand Down
36 changes: 36 additions & 0 deletions apiUtils/headerLanguage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export default function headerLookup(req) {
let found;

if (typeof req !== "undefined") {
const { headers } = req;
if (!headers) return found;

const locales = [];
const acceptLanguage = headers["accept-language"];

if (acceptLanguage) {
const lngs = []; let i; let m;
const rgx = /(([a-z]{2})-?([A-Z]{2})?)\s*;?\s*(q=([0-9.]+))?/gi;

do {
m = rgx.exec(acceptLanguage);
if (m) {
const lng = m[1]; const weight = m[5] || "1"; const q = Number(weight);
if (lng && !isNaN(q)) {
lngs.push({ lng, q });
}
}
} while (m);

lngs.sort((a, b) => b.q - a.q);

for (i = 0; i < lngs.length; i++) {
locales.push(lngs[i].lng);
}

if (locales.length) found = locales;
}
}

return found;
}
51 changes: 51 additions & 0 deletions apiUtils/localeMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import headerLanguage from "./headerLanguage";
import redirect from "./redirect";

export default (req, res) => {
const {
query: { slug },
_parsedUrl
} = req;

const fallback = "de";
const allowedLocales = [
{ name: "de-DE", locale: "de" },
{ name: "de", locale: "de" },
{ name: "en-AU", locale: "en" },
{ name: "en-IN", locale: "en" },
{ name: "en-CA", locale: "en" },
{ name: "en-NZ", locale: "en" },
{ name: "en-US", locale: "en" },
{ name: "en-ZA", locale: "en" },
{ name: "en-GB", locale: "en" },
{ name: "en", locale: "en" }
];

const detections = headerLanguage(req);

let found;

if (detections && detections.length) {
detections.forEach((language) => {
if (found || typeof language !== "string") return;

const lookedUpLocale = allowedLocales.find((allowedLocale) => allowedLocale.name === language);

if (lookedUpLocale) {
found = lookedUpLocale.locale;
}
});
}

if (!found) {
found = fallback;
}

const queryPart = (_parsedUrl && _parsedUrl.query) ? `?${_parsedUrl.query}` : "";

if (slug) {
return redirect(res, 302, `/${found}${slug ? `/${slug.join("/")}` : ""}${queryPart}`);
}

return redirect(res, 302, `/${found}${queryPart}`);
};
53 changes: 53 additions & 0 deletions apiUtils/passportMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import OAuth2Strategy from "passport-oauth2";
import passport from "passport";
import sessions from "client-sessions";
import appConfig from "../config.js";
import redirect from "./redirect";

export { default as passport } from "passport";

let baseUrl = appConfig.CANONICAL_URL;
if (!baseUrl.endsWith("/")) baseUrl = `${baseUrl}/`;

const oauthRedirectUrl = `${baseUrl}callback`;

// This is needed to allow custom parameters (e.g. loginActions) to be included
// when requesting authorization. This is setup to allow only loginAction to pass through
OAuth2Strategy.prototype.authorizationParams = function (options = {}) {
return { loginAction: options.loginAction };
};

passport.use("oauth2", new OAuth2Strategy({
authorizationURL: appConfig.OAUTH2_AUTH_URL,
tokenURL: appConfig.OAUTH2_TOKEN_URL,
clientID: appConfig.OAUTH2_CLIENT_ID,
clientSecret: appConfig.OAUTH2_CLIENT_SECRET,
callbackURL: oauthRedirectUrl,
state: true,
scope: ["offline", "openid"]
}, (accessToken, refreshToken, params, profile, cb) => {
cb(null, { accessToken, refreshToken, idToken: params.id_token });
}));

passport.serializeUser((user, done) => {
done(null, JSON.stringify(user));
});

passport.deserializeUser((user, done) => {
done(null, JSON.parse(user));
});

export default (handler) => (req, res) => {
if (!res.redirect) {
res.redirect = (location) => redirect(res, 302, location);
}

sessions({
cookieName: "session", // This name is required so passport picks it up correctly
secret: appConfig.SESSION_SECRET,
duration: appConfig.SESSION_MAX_AGE_MS
})(req, res, () =>
passport.initialize()(req, res, () =>
passport.session()(req, res, () =>
handler(req, res))));
};
17 changes: 17 additions & 0 deletions apiUtils/redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function redirect(res, statusCode, location) {
if (!res) {
throw new Error("Response object required");
}

if (!statusCode) {
throw new Error("Status code required");
}

if (!location) {
throw new Error("Location required");
}

res.statusCode = statusCode;
res.setHeader("Location", location);
res.end();
}
95 changes: 95 additions & 0 deletions components/AccountDropdown/AccountDropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState, Fragment } from "react";
import inject from "hocs/inject";
import { makeStyles } from "@material-ui/core/styles";
import IconButton from "@material-ui/core/IconButton";
import Button from "@material-ui/core/Button";
import ButtonBase from "@material-ui/core/ButtonBase";
import AccountIcon from "mdi-material-ui/Account";
import Popover from "@material-ui/core/Popover";
import useViewer from "hooks/viewer/useViewer";
import ViewerInfo from "@reactioncommerce/components/ViewerInfo/v1";
import Link from "components/Link";

const useStyles = makeStyles((theme) => ({
accountDropdown: {
width: 320,
padding: theme.spacing(2)
},
marginBottom: {
marginBottom: theme.spacing(2)
}
}));

const AccountDropdown = () => {
const classes = useStyles();
const [anchorElement, setAnchorElement] = useState(null);
const [viewer, isLoadingViewer] = useViewer();
const isAuthenticated = viewer && viewer._id;

const toggleOpen = (event) => {
setAnchorElement(event.currentTarget);
};

const onClose = () => {
setAnchorElement(null);
};

return (
<Fragment>
{ isAuthenticated ?
<ButtonBase onClick={toggleOpen}>
<ViewerInfo viewer={viewer} />
</ButtonBase>
:
<IconButton color="inherit" onClick={toggleOpen}>
<AccountIcon />
</IconButton>
}

<Popover
anchorEl={anchorElement}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
open={Boolean(anchorElement)}
onClose={onClose}
>
<div className={classes.accountDropdown}>
{isAuthenticated ?
<Fragment>
<div className={classes.marginBottom}>
<Link href="/profile/address">
<Button color="primary" fullWidth>
Profile
</Button>
</Link>
</div>
<div className={classes.marginBottom}>
<Button color="primary" fullWidth href={`/change-password?email=${encodeURIComponent(viewer.emailRecords[0].address)}`}>
Change Password
</Button>
</div>
<Button color="primary" fullWidth href="/logout" variant="contained">
Sign Out
</Button>
</Fragment>
:
<Fragment>
<div className={classes.authContent}>
<Button color="primary" fullWidth href="/signin" variant="contained">
Sign In
</Button>
</div>
<Button color="primary" fullWidth href="/signup">
Create Account
</Button>
</Fragment>
}
</div>
</Popover>
</Fragment>
);
};

export default inject("authStore")(AccountDropdown);
File renamed without changes.
Loading

0 comments on commit d0840ee

Please sign in to comment.