diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..a37fabf --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +NEXT_PUBLIC_READING_BASE_URI=https://www.oyvindis.com \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..a37fabf --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +NEXT_PUBLIC_READING_BASE_URI=https://www.oyvindis.com \ No newline at end of file diff --git a/components/container/index.tsx b/components/container/index.tsx new file mode 100644 index 0000000..b32ae31 --- /dev/null +++ b/components/container/index.tsx @@ -0,0 +1,11 @@ +import React, { memo, FC, PropsWithChildren } from 'react'; + +import SC from './styled'; + +interface Props {} + +const Container: FC> = ({ children }) => ( + {children} +); + +export default memo(Container); diff --git a/components/container/styled.ts b/components/container/styled.ts new file mode 100644 index 0000000..0cad215 --- /dev/null +++ b/components/container/styled.ts @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const onDesktopView = '@media (min-width: 900px)'; + +const Container = styled.div` + display: flex; + flex-direction: column; + margin: 0 auto; + padding: 0 calc(12px + (32 - 12) * ((100vw - 320px) / (900 - 320))); + width: 1200px; + z-index: 10; + + ${onDesktopView} { + & { + padding: 0; + } + } +`; + +export default { Container }; diff --git a/components/pages/climate/index.tsx b/components/pages/climate/index.tsx new file mode 100644 index 0000000..73afdf7 --- /dev/null +++ b/components/pages/climate/index.tsx @@ -0,0 +1,27 @@ +import React, { memo, FC } from 'react'; +import Link from 'next/link'; + +import Container from '../../container'; +import SC from './styled'; +import { Location } from '../../../types'; + +interface Props { + locations: Location[]; +} + +const ClimatePageWrapper: FC = ({ locations }) => ( + + +
    + {locations?.length > 0 && + locations.map(({ id, name }) => ( +
  • + {name} +
  • + ))} +
+
+
+); + +export default memo(ClimatePageWrapper); diff --git a/components/pages/climate/styled.ts b/components/pages/climate/styled.ts new file mode 100644 index 0000000..1564028 --- /dev/null +++ b/components/pages/climate/styled.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +const ClimatePage = styled.div` + display: flex; +`; + +export default { ClimatePage }; diff --git a/env.ts b/env.ts new file mode 100644 index 0000000..1f66c6c --- /dev/null +++ b/env.ts @@ -0,0 +1,11 @@ +import { validateEnv } from './utils/common'; + +const NAMESPACE = process.env.NAMESPACE! ?? 'dev'; +const READING_BASE_URI = process.env.NEXT_PUBLIC_READING_BASE_URI! ?? 'https://www.oyvindis.com' + +const env = { + NAMESPACE, + READING_BASE_URI +} + +export default validateEnv(env); diff --git a/package-lock.json b/package-lock.json index 9fb78dc..82ae2af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "climate-gui", "version": "0.1.0", "dependencies": { + "axios": "^0.26.1", "next": "12.1.0", "react": "17.0.2", "react-dom": "17.0.2", @@ -18,6 +19,7 @@ "@types/react": "^17.0.43", "@types/styled-components": "^5.1.24", "@typescript-eslint/eslint-plugin": "^5.18.0", + "babel-plugin-styled-components": "^2.0.6", "eslint": "8.11.0", "eslint-config-next": "12.1.0", "eslint-config-prettier": "^8.5.0", @@ -1140,6 +1142,14 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -2006,6 +2016,25 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4356,6 +4385,14 @@ "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", "dev": true }, + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "requires": { + "follow-redirects": "^1.14.8" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -5025,6 +5062,11 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/package.json b/package.json index d3282d9..344d522 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint --fix" }, "dependencies": { + "axios": "^0.26.1", "next": "12.1.0", "react": "17.0.2", "react-dom": "17.0.2", diff --git a/pages/climate/index.tsx b/pages/climate/index.tsx new file mode 100644 index 0000000..5242abb --- /dev/null +++ b/pages/climate/index.tsx @@ -0,0 +1,23 @@ +import type { NextPage, GetServerSidePropsResult } from 'next'; + +import { getLocations } from '../../services/api/climate/locations'; + +import ClimatePageWrapper from '../../components/pages/climate'; +import { Location } from '../../types'; + +interface Props { + locations: Location[]; +} + +const Climate: NextPage = ({ locations }) => { + return ; +}; + +export async function getServerSideProps(): Promise< + GetServerSidePropsResult +> { + const locations = await getLocations(); + return { props: { locations } }; +} + +export default Climate; diff --git a/pages/climate/location/[id]/index.tsx b/pages/climate/location/[id]/index.tsx new file mode 100644 index 0000000..c596ff4 --- /dev/null +++ b/pages/climate/location/[id]/index.tsx @@ -0,0 +1,38 @@ +import type { NextPage } from 'next'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +import { Reading } from '../../../../types'; +import { getReadings } from '../../../../services/api/climate/readings'; + +interface Props {} + +const Location: NextPage = () => { + const [readings, setReadings] = useState([]); + + const router = useRouter(); + const { id } = router.query; + + useEffect(() => { + getReadings(Array.isArray(id) ? id[0] : id).then(readings => + setReadings(readings) + ); + }, [id]); + + return ( +
+
    + {readings?.length > 0 && + readings.map(({ id, humidity, temp, localDateTime }) => ( +
  • +
    + {temp} - {humidity} - {localDateTime} +
    +
  • + ))} +
+
+ ); +}; + +export default Location; diff --git a/services/api/climate/host.ts b/services/api/climate/host.ts new file mode 100644 index 0000000..fedfe06 --- /dev/null +++ b/services/api/climate/host.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; +import env from '../../../env'; + +interface Props { + path: string; + method: any; + data?: any; + params?: URLSearchParams; +} + +const { READING_BASE_URI } = env; + +export const climateReading = ({ path, method, data, params }: Props) => + axios({ + url: `${READING_BASE_URI}${path}`, + method, + data, + params + }) + .then(response => response.data) + .catch(() => null); + +export const climateReadingPost = (path: string, body: any) => + climateReading({ path, method: 'POST', data: body }); + +export const climateReadingGet = (path: string, params?: URLSearchParams) => + climateReading({ path, method: 'GET', params }); diff --git a/services/api/climate/locations.ts b/services/api/climate/locations.ts new file mode 100644 index 0000000..ab1f33f --- /dev/null +++ b/services/api/climate/locations.ts @@ -0,0 +1,4 @@ +import { climateReadingGet } from './host'; + +export const getLocations = () => + climateReadingGet('/climate-api/location') diff --git a/services/api/climate/readings.ts b/services/api/climate/readings.ts new file mode 100644 index 0000000..4fa3ba3 --- /dev/null +++ b/services/api/climate/readings.ts @@ -0,0 +1,4 @@ +import { climateReadingGet } from './host'; + +export const getReadings = (location: string) => + climateReadingGet(`/climate-api/reading/${location}`) diff --git a/types/common.d.ts b/types/common.d.ts new file mode 100644 index 0000000..93f7b9a --- /dev/null +++ b/types/common.d.ts @@ -0,0 +1,3 @@ +export type Actions any }> = { + [K in keyof T]: ReturnType; +}[keyof T]; diff --git a/types/domain.d.ts b/types/domain.d.ts new file mode 100644 index 0000000..02c3806 --- /dev/null +++ b/types/domain.d.ts @@ -0,0 +1,19 @@ +export interface Location { + id: string; + name: string; +} + +export interface Reading { + id: string; + location: string; + localDateTime: string; + battery: string; + co2: string; + humidity: string; + pm1: string; + pm25: string; + pressure: string; + temp: string; + time: string; + voc: string; +} diff --git a/types/enums.ts b/types/enums.ts new file mode 100644 index 0000000..0aa0e77 --- /dev/null +++ b/types/enums.ts @@ -0,0 +1,3 @@ +export enum Namespace { + DEVELOPMENT = 'dev' +} diff --git a/types/env.d.ts b/types/env.d.ts new file mode 100644 index 0000000..832809e --- /dev/null +++ b/types/env.d.ts @@ -0,0 +1,4 @@ +export interface EnvironmentVariables { + NAMESPACE: string; + READING_BASE_URI: string; +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..182bc7c --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,3 @@ +export * from './env'; +export * from './common'; +export * from './domain'; diff --git a/utils/common/index.ts b/utils/common/index.ts new file mode 100755 index 0000000..452224c --- /dev/null +++ b/utils/common/index.ts @@ -0,0 +1,17 @@ +import type { EnvironmentVariables } from '../../types'; + +function assertIsDefined( + key: string, + value: T +): asserts value is NonNullable { + if (value === undefined || value === null) { + throw new Error(`Expected ${key} to be defined, but received ${value}`); + } +} + +export const validateEnv = ( + env: EnvironmentVariables +): EnvironmentVariables => { + Object.entries(env).forEach(([key, value]) => assertIsDefined(key, value)); + return env; +};