Skip to content

Commit 0e77bd1

Browse files
committed
Initial commit
1 parent ed74c9e commit 0e77bd1

16 files changed

+6407
-0
lines changed

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Postier
2+
3+
## Context
4+
5+
Postier is the french term for postman.
6+
7+
This project is built out of the frustration using La Poste's [tracking service](https://www.laposte.fr/outils/suivre-vos-envois).
8+
9+
## Getting Started
10+
11+
### Development
12+
13+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app). It uses [Material-UI](https://material-ui.com/) components.
14+
15+
Get an API key from La Poste's [API Suivi](https://developer.laposte.fr/products/suivi/2) and add it in a `.env` file at the root of the project:
16+
17+
```
18+
OKAPI_KEY='YOUR_KEY'
19+
```
20+
21+
Then, run the development server:
22+
23+
```bash
24+
yarn install
25+
yarn dev
26+
```
27+
28+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

components/deliveryTimeline.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from "react";
2+
import StepIcon from "./stepIcon";
3+
import { CARD_HEIGHT } from "../pages";
4+
import { Grid, makeStyles, Typography } from "@material-ui/core";
5+
6+
const formatDate = (dateStr) => {
7+
const date = new Date(dateStr);
8+
const options = {
9+
month: "short",
10+
day: "numeric",
11+
hour: "numeric",
12+
minute: "numeric",
13+
};
14+
15+
return new Intl.DateTimeFormat("en", options).format(date);
16+
};
17+
18+
const useStyles = makeStyles((theme) => ({
19+
container: {
20+
marginBottom: "5px",
21+
},
22+
label: {
23+
paddingBottom: "8px",
24+
},
25+
line: {
26+
height: "100%",
27+
marginLeft: "9px",
28+
borderLeftWidth: "1px",
29+
borderLeftStyle: "solid",
30+
borderLeftColor: theme.palette.primary.main,
31+
},
32+
}));
33+
34+
export default function DeliveryTimeline({ events }) {
35+
const classes = useStyles();
36+
const ids = Object.keys(events);
37+
38+
return (
39+
<Grid container spacing={2}>
40+
{ids.map((id, index) => {
41+
return (
42+
<Grid key={index} className={classes.container} container>
43+
<Grid item xs={1}>
44+
<StepIcon code={events[id].code} />
45+
</Grid>
46+
<Grid item xs={11}>
47+
<Typography variant="body2" color="textSecondary">
48+
{formatDate(events[id].date)}
49+
</Typography>
50+
</Grid>
51+
<Grid item xs={1}>
52+
<div className={classes.line} />
53+
</Grid>
54+
<Grid className={classes.label} item xs={11}>
55+
<Typography variant="body1" color="textPrimary">
56+
{events[id].label}
57+
</Typography>
58+
</Grid>
59+
</Grid>
60+
);
61+
})}
62+
</Grid>
63+
);
64+
}

components/errorCard.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, { useState } from "react";
2+
import { CARD_HEIGHT } from "../pages";
3+
import CloseIcon from "@material-ui/icons/Close";
4+
import { Alert, AlertTitle } from "@material-ui/lab";
5+
import { IconButton, Typography } from "@material-ui/core";
6+
7+
export default function ErrorCard({ info, onDelete }) {
8+
const { idShip, returnCode, returnMessage } = info;
9+
10+
return (
11+
<Alert
12+
action={
13+
<IconButton
14+
aria-label="close"
15+
color="inherit"
16+
onClick={onDelete}
17+
size="small"
18+
>
19+
<CloseIcon fontSize="inherit" />
20+
</IconButton>
21+
}
22+
severity="error"
23+
style={{ minHeight: CARD_HEIGHT }}
24+
>
25+
<AlertTitle>Error</AlertTitle>
26+
{returnCode ? (
27+
<Typography variant={"body2"}>Code Error: {returnCode}</Typography>
28+
) : null}
29+
{idShip ? (
30+
<Typography variant={"body2"}>Tracking Id: {idShip}</Typography>
31+
) : null}
32+
{returnMessage ? (
33+
<Typography variant={"body2"}>Message: {returnMessage}</Typography>
34+
) : null}
35+
</Alert>
36+
);
37+
}

components/footer.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
import { Grid, makeStyles, Link, Typography } from "@material-ui/core";
3+
import GitHubIcon from "@material-ui/icons/GitHub";
4+
import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder";
5+
import CodeIcon from "@material-ui/icons/Code";
6+
7+
const useStyles = makeStyles((theme) => ({
8+
footer: {
9+
width: "100%",
10+
height: "30px",
11+
marginTop: "20px",
12+
borderTop: "1px solid #eaeaea",
13+
display: "flex",
14+
},
15+
icon: {
16+
verticalAlign: "bottom",
17+
},
18+
}));
19+
20+
export default function Footer() {
21+
const classes = useStyles();
22+
23+
return (
24+
<footer className={classes.footer}>
25+
<Grid alignItems="center" container direction="row">
26+
<Grid item xs={6}>
27+
<Typography align="left" color="textSecondary" variant="body2">
28+
<CodeIcon
29+
aria-label="coded"
30+
classes={{ root: classes.icon }}
31+
fontSize="small"
32+
/>{" "}
33+
{" with "}
34+
<FavoriteBorderIcon
35+
aria-label="love"
36+
classes={{ root: classes.icon }}
37+
fontSize="small"
38+
/>
39+
{" by "}
40+
<Link color="inherit" href="https://twitter.com/vjo">
41+
@vjo
42+
</Link>
43+
{"."}
44+
</Typography>
45+
</Grid>
46+
<Grid item xs={6}>
47+
<Typography align="right" color="textSecondary" variant="body2">
48+
<Link color="inherit" href="https://github.com/vjo/postier">
49+
<GitHubIcon
50+
aria-label="Github"
51+
classes={{ root: classes.icon }}
52+
fontSize="small"
53+
/>
54+
</Link>
55+
</Typography>
56+
</Grid>
57+
</Grid>
58+
</footer>
59+
);
60+
}

components/packageInfoCard.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React, { useState } from "react";
2+
import DeliveryTimeline from "./deliveryTimeline";
3+
import CloseIcon from "@material-ui/icons/Close";
4+
import LinkIcon from "@material-ui/icons/Link";
5+
import RefreshIcon from "@material-ui/icons/Refresh";
6+
import {
7+
Button,
8+
Card,
9+
CardActions,
10+
CardContent,
11+
CardHeader,
12+
Chip,
13+
Collapse,
14+
Grid,
15+
IconButton,
16+
LinearProgress,
17+
Link,
18+
makeStyles,
19+
Typography,
20+
} from "@material-ui/core";
21+
22+
const LINEAR_PROGRESS_HEIGHT = "4px";
23+
24+
const useStyles = makeStyles((theme) => ({
25+
showMore: {
26+
marginLeft: "auto",
27+
},
28+
}));
29+
30+
export default function PackageInfoCard({ info, onDelete, onRefresh }) {
31+
const classes = useStyles();
32+
const [expanded, setExpanded] = useState(false);
33+
const handleExpandClick = () => {
34+
setExpanded(!expanded);
35+
};
36+
37+
const title = (
38+
<Grid container spacing={2}>
39+
<Grid item>
40+
<Typography variant="h5">{info.shipment.idShip}</Typography>
41+
</Grid>
42+
<Grid item>
43+
<Chip
44+
color="secondary"
45+
label={info.shipment.product}
46+
variant="outlined"
47+
/>
48+
</Grid>
49+
</Grid>
50+
);
51+
const subheader = `${info.shipment.event[0].label}`;
52+
53+
return (
54+
<Card>
55+
<CardHeader
56+
action={
57+
<IconButton aria-label="remove" onClick={onDelete}>
58+
<CloseIcon />
59+
</IconButton>
60+
}
61+
subheader={subheader}
62+
title={title}
63+
/>
64+
<CardActions disableSpacing>
65+
<IconButton aria-label="refresh information" onClick={onRefresh}>
66+
<RefreshIcon />
67+
</IconButton>
68+
<Link href={info.shipment.url} rel="noopener" target="_blank">
69+
<IconButton aria-label="view on carrier website">
70+
<LinkIcon />
71+
</IconButton>
72+
</Link>
73+
<Button
74+
className={classes.showMore}
75+
onClick={handleExpandClick}
76+
size="small"
77+
>
78+
{expanded ? "Show Less" : "Show More"}
79+
</Button>
80+
</CardActions>
81+
<Collapse in={expanded} timeout="auto" unmountOnExit>
82+
<CardContent>
83+
<DeliveryTimeline events={info.shipment.event} />
84+
</CardContent>
85+
</Collapse>
86+
{info.isRefreshing ? (
87+
<LinearProgress color="secondary" />
88+
) : (
89+
<div style={{ height: LINEAR_PROGRESS_HEIGHT }} />
90+
)}
91+
</Card>
92+
);
93+
}

components/stepIcon.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from "react";
2+
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
3+
import HighlightOffIcon from "@material-ui/icons/HighlightOff";
4+
import LensIcon from "@material-ui/icons/Lens";
5+
6+
const DELIVERED_CODE = "DI1";
7+
const RETURNED_CODE = "DI2";
8+
9+
export default function StepIcon({ code }) {
10+
switch (code) {
11+
case DELIVERED_CODE:
12+
return <CheckCircleIcon color="primary" fontSize="small" />;
13+
case RETURNED_CODE:
14+
return <HighlightOffIcon color="primary" fontSize="small" />;
15+
default:
16+
return <LensIcon color="primary" fontSize="small" />;
17+
}
18+
}

components/theme.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createMuiTheme } from "@material-ui/core/styles";
2+
import { red } from "@material-ui/core/colors";
3+
4+
const theme = createMuiTheme({
5+
palette: {
6+
primary: {
7+
main: "#ffc928",
8+
},
9+
secondary: {
10+
main: "#20458f",
11+
},
12+
error: {
13+
main: red.A400,
14+
},
15+
background: {
16+
default: "#fff",
17+
},
18+
},
19+
});
20+
21+
export default theme;

next.config.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// On production, variables are set with `now secrets`. On development, they use the .env file
2+
require("dotenv").config();
3+
4+
module.exports = {
5+
env: {
6+
OKAPI_KEY: process.env.OKAPI_KEY,
7+
},
8+
};

package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "postier",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start"
9+
},
10+
"dependencies": {
11+
"@material-ui/core": "false4.9.14",
12+
"@material-ui/icons": "false4.9.1",
13+
"@material-ui/lab": "false4.0.0-alpha.53",
14+
"@material-ui/styles": "false4.9.14",
15+
"dotenv": "false8.2.0",
16+
"next": "9.4.1",
17+
"react": "16.13.1",
18+
"react-dom": "16.13.1"
19+
}
20+
}

pages/_app.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import Head from "next/head";
4+
import { ThemeProvider } from "@material-ui/core/styles";
5+
import CssBaseline from "@material-ui/core/CssBaseline";
6+
import theme from "../components/theme";
7+
8+
export default function MyApp(props) {
9+
const { Component, pageProps } = props;
10+
11+
React.useEffect(() => {
12+
// Remove the server-side injected CSS.
13+
const jssStyles = document.querySelector("#jss-server-side");
14+
if (jssStyles) {
15+
jssStyles.parentElement.removeChild(jssStyles);
16+
}
17+
}, []);
18+
19+
return (
20+
<React.Fragment>
21+
<Head>
22+
<title>Postier - Track LaPoste deliveries</title>
23+
<meta
24+
name="viewport"
25+
content="minimum-scale=1, initial-scale=1, width=device-width"
26+
/>
27+
</Head>
28+
<ThemeProvider theme={theme}>
29+
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
30+
<CssBaseline />
31+
<Component {...pageProps} />
32+
</ThemeProvider>
33+
</React.Fragment>
34+
);
35+
}
36+
37+
MyApp.propTypes = {
38+
Component: PropTypes.elementType.isRequired,
39+
pageProps: PropTypes.object.isRequired,
40+
};

0 commit comments

Comments
 (0)