From 64a3ec50dc24c9476bacc72b0c9c8887da2d844e Mon Sep 17 00:00:00 2001 From: DiD3n Date: Wed, 2 Feb 2022 14:56:19 +0100 Subject: [PATCH 01/16] Add code base --- src/common/lectures/LecturesContext.tsx | 21 +++++++++++++++++++++ src/common/lectures/service/Lectures.ts | 14 ++++++++++++++ src/common/models/Agenda.model.ts | 8 ++++++++ src/common/models/Lecture.model.ts | 16 ++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 src/common/lectures/LecturesContext.tsx create mode 100644 src/common/lectures/service/Lectures.ts create mode 100644 src/common/models/Agenda.model.ts create mode 100644 src/common/models/Lecture.model.ts diff --git a/src/common/lectures/LecturesContext.tsx b/src/common/lectures/LecturesContext.tsx new file mode 100644 index 0000000..0155851 --- /dev/null +++ b/src/common/lectures/LecturesContext.tsx @@ -0,0 +1,21 @@ +import React, {createContext} from 'react'; + +interface LecturesContextInteraface { + lectures: LecturesContextInteraface[], +} + +export const LecturesContext = createContext({ + lectures: [] +}); + +export const LectureProvider: React.FC = ({/* axios ,*/ children}) => { + + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/common/lectures/service/Lectures.ts b/src/common/lectures/service/Lectures.ts new file mode 100644 index 0000000..5153517 --- /dev/null +++ b/src/common/lectures/service/Lectures.ts @@ -0,0 +1,14 @@ +import Axios, {AxiosInstance} from 'axios'; +import {Lecture, SimpleLecture} from "../../models/Lecture.model"; + +export const fetchLecturesList = async (axios: AxiosInstance, page: number, limit: number) => + axios.get('/api/v1/lectures', { + params: { + page, limit + } + }) + .then(res => res.data); + +export const fetchLectureById = async (axios: AxiosInstance, id: string) => + axios.get(`/api/v1/lecture/${id}`) + .then(res => res.data); diff --git a/src/common/models/Agenda.model.ts b/src/common/models/Agenda.model.ts new file mode 100644 index 0000000..c8fe561 --- /dev/null +++ b/src/common/models/Agenda.model.ts @@ -0,0 +1,8 @@ + +export interface AgendaEntry { + name: string + description?: string + + startDate: Date + endDate: Date +} \ No newline at end of file diff --git a/src/common/models/Lecture.model.ts b/src/common/models/Lecture.model.ts new file mode 100644 index 0000000..226ca34 --- /dev/null +++ b/src/common/models/Lecture.model.ts @@ -0,0 +1,16 @@ +import {AgendaEntry} from "./Agenda.model"; + +export interface SimpleLecture { + id: string + + name: string + description: string + + startDate: string + finishDate: string +} + + +export interface Lecture extends SimpleLecture { + agenda?: AgendaEntry[] +} \ No newline at end of file From 9825d19cfa40eb059650c9d23370074b63d7d710 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Tue, 8 Feb 2022 18:20:04 +0100 Subject: [PATCH 02/16] Add uniform network resource wrapper --- src/common/NetworkResource.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/common/NetworkResource.ts diff --git a/src/common/NetworkResource.ts b/src/common/NetworkResource.ts new file mode 100644 index 0000000..54b56f4 --- /dev/null +++ b/src/common/NetworkResource.ts @@ -0,0 +1,16 @@ + +export enum ResourceStatus { + Pending, + + // every other state is treated as true + // it's this way bc every other state/value + // is already done request but with different result + // any 'if' check on resource should be resolved in two ways + // > resource is pending/loading (and placeholder should be displayed) + // > resource is fetched with different results (valid or not valid one) + NotFound, + NotAuthorized, + Error, +} + +export type NetworkResource = ResourceStatus | T From 3dfb3784126bc95d13edf6ce791cafb7b7a64912 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Wed, 9 Feb 2022 16:06:05 +0100 Subject: [PATCH 03/16] Add code --- src/common/hooks/useMap.ts | 12 +++++++ src/common/lectures/LecturesContext.tsx | 33 +++++++++++++++---- src/common/models/Lecture.model.ts | 43 ++++++++++++++++++------- 3 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 src/common/hooks/useMap.ts diff --git a/src/common/hooks/useMap.ts b/src/common/hooks/useMap.ts new file mode 100644 index 0000000..a2f367f --- /dev/null +++ b/src/common/hooks/useMap.ts @@ -0,0 +1,12 @@ +import { useState } from "react"; + +export const useMap = () => { + const [store, setStore] = useState>(new Map()); + + return { + get: (key: K) => store.get(key), + has: (key: K) => store.has(key), + set: (key: K, val: T) => + setStore( store => store.set(key, val)), + } +} \ No newline at end of file diff --git a/src/common/lectures/LecturesContext.tsx b/src/common/lectures/LecturesContext.tsx index 0155851..edb24d8 100644 --- a/src/common/lectures/LecturesContext.tsx +++ b/src/common/lectures/LecturesContext.tsx @@ -1,19 +1,40 @@ import React, {createContext} from 'react'; +import {Lecture, SimpleLecture} from "../models/Lecture.model"; +import {fetchLectureById} from "./service/Lectures"; +import axios from "axios"; +import {NetworkResource, ResourceStatus} from "../NetworkResource"; -interface LecturesContextInteraface { - lectures: LecturesContextInteraface[], +interface LecturesContextInterface { + lectures: LecturesContextInterface[] + + get(id: string): NetworkResource + getPage(page: number, limit: number): NetworkResource +} +const defaultLectureContext: LecturesContextInterface = { + lectures: [], + + get: () => ResourceStatus.Error, + getPage: () => ResourceStatus.Error, } -export const LecturesContext = createContext({ - lectures: [] -}); +export const LecturesContext = createContext(defaultLectureContext); export const LectureProvider: React.FC = ({/* axios ,*/ children}) => { + const getLecture = (id: string): NetworkResource => { + + if (id) { + fetchLectureById(axios, id) + .catch(console.error) + } + + return ResourceStatus.Pending; + } + return ( {children} diff --git a/src/common/models/Lecture.model.ts b/src/common/models/Lecture.model.ts index 226ca34..feb4322 100644 --- a/src/common/models/Lecture.model.ts +++ b/src/common/models/Lecture.model.ts @@ -1,16 +1,35 @@ import {AgendaEntry} from "./Agenda.model"; +import {User} from "./User"; -export interface SimpleLecture { - id: string - - name: string - description: string - - startDate: string - finishDate: string +export class SimpleLecture { + constructor( + public id: string, + public name: string, + public description: string, + public startDate: string, + ) { + } } - -export interface Lecture extends SimpleLecture { - agenda?: AgendaEntry[] -} \ No newline at end of file +export class Lecture extends SimpleLecture { + constructor( + public id: string, + public name: string, + public description: string, + public startDate: string, + public finishDate: string, + public city: string, + public place: string, + public latitude: number, + public longitude: number, + public agenda: AgendaEntry[] = [], + public author?: User, + ) { + super( + id, + name, + description, + startDate + ); + } +} From 430f1c33709cd839fe5257c142e9df37f3e478ce Mon Sep 17 00:00:00 2001 From: DiD3n Date: Fri, 25 Feb 2022 14:47:17 +0100 Subject: [PATCH 04/16] WIP: Add LectureCard component --- package-lock.json | 269 ++++++++++++++++-- package.json | 4 +- src/assets/images/icons/clock.svg | 3 + src/assets/images/icons/location.svg | 3 + .../containers/LectureCard/LectureCard.css | 60 ++++ .../containers/LectureCard/LectureCard.tsx | 58 ++++ .../LectureCard/chips/LocationChip.tsx | 13 + .../LectureCard/chips/StartDateChip.tsx | 16 ++ .../LectureCard/chips/chipStyles.css | 14 + src/index.css | 8 + src/index.tsx | 11 +- src/views/home/index.tsx | 11 + webpack.config.js | 5 +- 13 files changed, 448 insertions(+), 27 deletions(-) create mode 100644 src/assets/images/icons/clock.svg create mode 100644 src/assets/images/icons/location.svg create mode 100644 src/components/containers/LectureCard/LectureCard.css create mode 100644 src/components/containers/LectureCard/LectureCard.tsx create mode 100644 src/components/containers/LectureCard/chips/LocationChip.tsx create mode 100644 src/components/containers/LectureCard/chips/StartDateChip.tsx create mode 100644 src/components/containers/LectureCard/chips/chipStyles.css diff --git a/package-lock.json b/package-lock.json index c5385b9..0a24333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.2", "license": "ISC", "dependencies": { + "@apollo/client": "^3.5.9", "@babel/runtime": "^7.15.4", "express": "^4.17.1", - "express-history-api-fallback": "^2.2.1" + "express-history-api-fallback": "^2.2.1", + "graphql": "^16.3.0" }, "devDependencies": { "@babel/cli": "^7.15.7", @@ -55,6 +57,38 @@ "webpackbar": "^5.0.0-3" } }, + "node_modules/@apollo/client": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.9.tgz", + "integrity": "sha512-Qq3OE3GpyPG2fYXBzi1n4QXcKZ11c6jHdrXK2Kkn9SD+vUymSrllXsldqnKUK9tslxKqkKzNrkCXkLv7PxwfSQ==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.0.0", + "@wry/context": "^0.6.0", + "@wry/equality": "^0.5.0", + "@wry/trie": "^0.3.0", + "graphql-tag": "^2.12.3", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.16.1", + "prop-types": "^15.7.2", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.9.4", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, "node_modules/@babel/cli": { "version": "7.15.7", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.15.7.tgz", @@ -1911,6 +1945,14 @@ "node": ">= 4" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -3595,6 +3637,39 @@ } } }, + "node_modules/@wry/context": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", + "integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.2.tgz", + "integrity": "sha512-oVMxbUXL48EV/C0/M7gLVsoK6qRHPS85x8zECofEZOVvxGmIPLA9o5Z27cc2PoAyZz1S2VoM2A7FLAnpfGlneA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.1.tgz", + "integrity": "sha512-WwB53ikYudh9pIorgxrkHKrQZcCqNM/Q/bDzZBffEaGUKGuHrRb3zZUT9Sh2qw9yogC7SsdRmQ1ER0pqvd3bfw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7317,6 +7392,28 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, + "node_modules/graphql": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", + "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || >=16.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -7441,7 +7538,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "dependencies": { "react-is": "^16.7.0" } @@ -10156,8 +10252,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -10519,7 +10614,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -11155,7 +11249,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11341,6 +11434,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optimism": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", + "integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==", + "dependencies": { + "@wry/context": "^0.6.0", + "@wry/trie": "^0.3.0" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -11842,7 +11944,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -11984,7 +12085,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, + "devOptional": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -12016,8 +12117,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-router": { "version": "6.2.2", @@ -13392,6 +13492,14 @@ "node": ">= 10" } }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -13762,6 +13870,17 @@ "node": ">=8" } }, + "node_modules/ts-invariant": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.4.tgz", + "integrity": "sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ts-loader": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.6.tgz", @@ -13922,8 +14041,7 @@ "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -14929,9 +15047,41 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "node_modules/zen-observable-ts": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz", + "integrity": "sha512-hc/TGiPkAWpByykMwDcem3SdUgA4We+0Qb36bItSuJC9xD0XVBZoFHYoadAomDSNf64CG8Ydj0Qb8Od8BUWz5g==", + "dependencies": { + "zen-observable": "0.8.15" + } } }, "dependencies": { + "@apollo/client": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.9.tgz", + "integrity": "sha512-Qq3OE3GpyPG2fYXBzi1n4QXcKZ11c6jHdrXK2Kkn9SD+vUymSrllXsldqnKUK9tslxKqkKzNrkCXkLv7PxwfSQ==", + "requires": { + "@graphql-typed-document-node/core": "^3.0.0", + "@wry/context": "^0.6.0", + "@wry/equality": "^0.5.0", + "@wry/trie": "^0.3.0", + "graphql-tag": "^2.12.3", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.16.1", + "prop-types": "^15.7.2", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.9.4", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.0" + } + }, "@babel/cli": { "version": "7.15.7", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.15.7.tgz", @@ -16216,6 +16366,12 @@ } } }, + "@graphql-typed-document-node/core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -17542,6 +17698,30 @@ "dev": true, "requires": {} }, + "@wry/context": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz", + "integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@wry/equality": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.2.tgz", + "integrity": "sha512-oVMxbUXL48EV/C0/M7gLVsoK6qRHPS85x8zECofEZOVvxGmIPLA9o5Z27cc2PoAyZz1S2VoM2A7FLAnpfGlneA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@wry/trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.1.tgz", + "integrity": "sha512-WwB53ikYudh9pIorgxrkHKrQZcCqNM/Q/bDzZBffEaGUKGuHrRb3zZUT9Sh2qw9yogC7SsdRmQ1ER0pqvd3bfw==", + "requires": { + "tslib": "^2.3.0" + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -20407,6 +20587,19 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, + "graphql": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", + "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==" + }, + "graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "requires": { + "tslib": "^2.1.0" + } + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -20495,7 +20688,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "requires": { "react-is": "^16.7.0" } @@ -22511,8 +22703,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.1", @@ -22795,7 +22986,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -23283,8 +23473,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { "version": "1.11.0", @@ -23413,6 +23602,15 @@ "is-wsl": "^2.2.0" } }, + "optimism": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", + "integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==", + "requires": { + "@wry/context": "^0.6.0", + "@wry/trie": "^0.3.0" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -23780,7 +23978,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -23880,7 +24077,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, + "devOptional": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -23906,8 +24103,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-router": { "version": "6.2.2", @@ -24986,6 +25182,11 @@ } } }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -25263,6 +25464,14 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, + "ts-invariant": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.4.tgz", + "integrity": "sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ==", + "requires": { + "tslib": "^2.1.0" + } + }, "ts-loader": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.6.tgz", @@ -25366,8 +25575,7 @@ "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tsutils": { "version": "3.21.0", @@ -26103,6 +26311,19 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz", + "integrity": "sha512-hc/TGiPkAWpByykMwDcem3SdUgA4We+0Qb36bItSuJC9xD0XVBZoFHYoadAomDSNf64CG8Ydj0Qb8Od8BUWz5g==", + "requires": { + "zen-observable": "0.8.15" + } } } } diff --git a/package.json b/package.json index 68ee718..b7e4799 100644 --- a/package.json +++ b/package.json @@ -121,8 +121,10 @@ "webpackbar": "^5.0.0-3" }, "dependencies": { + "@apollo/client": "^3.5.9", "@babel/runtime": "^7.15.4", "express": "^4.17.1", - "express-history-api-fallback": "^2.2.1" + "express-history-api-fallback": "^2.2.1", + "graphql": "^16.3.0" } } diff --git a/src/assets/images/icons/clock.svg b/src/assets/images/icons/clock.svg new file mode 100644 index 0000000..6cd2b08 --- /dev/null +++ b/src/assets/images/icons/clock.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/icons/location.svg b/src/assets/images/icons/location.svg new file mode 100644 index 0000000..8e99451 --- /dev/null +++ b/src/assets/images/icons/location.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/containers/LectureCard/LectureCard.css b/src/components/containers/LectureCard/LectureCard.css new file mode 100644 index 0000000..c3d5a8d --- /dev/null +++ b/src/components/containers/LectureCard/LectureCard.css @@ -0,0 +1,60 @@ +.link { + text-decoration: none; +} + +.card { + --padding: 16px; + + box-sizing: border-box; + position: relative; + + margin: 8px; + min-height: 113px; + padding: var(--padding); + + border-radius: 16px; + box-shadow: 0 2px 24px rgba(7, 16, 24, 0.3); +} + +.bgImg { + position: absolute; + height: 100%; + left: 0; + top: 0; + + border-radius: 16px; + opacity: .4; + + -webkit-mask-image: linear-gradient(90deg, #FFF 50%, transparent); + mask-image: linear-gradient(90deg, #FFF 50%, transparent); +} +.title { + position: relative; + padding: 0; + margin: 0; + + font-weight: bold; + font-size: 24px; + line-height: 29px; + + color: #FFFFFF; +} +.subtitle { + position: relative; + padding: 0; + margin: 0; + + font-size: 16px; + line-height: 19px; + font-weight: normal; + + color: rgba(255, 255, 255, 0.8); +} + +.bottom { + position: absolute; + display: flex; + + bottom: var(--padding); + left: var(--padding); +} \ No newline at end of file diff --git a/src/components/containers/LectureCard/LectureCard.tsx b/src/components/containers/LectureCard/LectureCard.tsx new file mode 100644 index 0000000..b75192f --- /dev/null +++ b/src/components/containers/LectureCard/LectureCard.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { Link } from 'react-router-dom' + +import styles from './LectureCard.css' +import {LocationChip} from "./chips/LocationChip"; +import {StartDateChip} from "./chips/StartDateChip"; + +interface LectureCardProps { + id: string, + title: string, + subtitle: string, + + color?: string, + image?: string, + + startDate: Date, + locationName?: string +} + +export const LectureCard: React.FC = ({ + id, + title, + subtitle, + color, + image, + startDate, + locationName +}) => { + + const cardStyle: React.CSSProperties = { + backgroundColor: color || "#080736" //space dark + } + + return ( + +
+ {image && + } +
+

+ {title} +

+

+ {subtitle} +

+ +
+
+ {locationName && + {locationName}} + + {startDate && + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/containers/LectureCard/chips/LocationChip.tsx b/src/components/containers/LectureCard/chips/LocationChip.tsx new file mode 100644 index 0000000..f5a5982 --- /dev/null +++ b/src/components/containers/LectureCard/chips/LocationChip.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import LocationIcon from '../../../../assets/images/icons/location.svg'; + +import styles from './chipStyles.css' + +export const LocationChip: React.FC = ({children}) => ( +
+ + + {children} + +
+) \ No newline at end of file diff --git a/src/components/containers/LectureCard/chips/StartDateChip.tsx b/src/components/containers/LectureCard/chips/StartDateChip.tsx new file mode 100644 index 0000000..904f37c --- /dev/null +++ b/src/components/containers/LectureCard/chips/StartDateChip.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import styles from "./chipStyles.css"; +import ClockIcon from "../../../../assets/images/icons/clock.svg"; + +interface StartDateChipProps { + date: Date +} + +export const StartDateChip: React.FC = ({date}) => ( +
+ + + {date.toDateString()} + +
+) \ No newline at end of file diff --git a/src/components/containers/LectureCard/chips/chipStyles.css b/src/components/containers/LectureCard/chips/chipStyles.css new file mode 100644 index 0000000..cb5d4df --- /dev/null +++ b/src/components/containers/LectureCard/chips/chipStyles.css @@ -0,0 +1,14 @@ + +.chip { + display: flex; + justify-content: center; + align-items: center; + margin-right: 16px; +} + +.text { + margin-left: 8px; + font-weight: 500; + font-size: 14px; + color: #FFFFFF; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 03afaff..e8879e7 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,12 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap'); + body { + font-family: 'Lato', sans-serif; + margin: 0; padding: 0; +} + +:root { + } \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index fac6f22..3ec350c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,20 @@ import React from 'react'; import { render } from 'react-dom'; +import {ApolloClient, ApolloProvider, InMemoryCache} from "@apollo/client"; import { App } from './components/App'; +const client = new ApolloClient({ + uri: 'https://48p1r2roz4.sse.codesandbox.io', + cache: new InMemoryCache() +}); + + import './index.css'; render( - , + + + , document.getElementById('root') ); diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx index 695f435..e1f2150 100644 --- a/src/views/home/index.tsx +++ b/src/views/home/index.tsx @@ -4,6 +4,9 @@ import { Link } from 'react-router-dom'; import { useAuth } from '../../common/auth/useAuth.hook'; import Planet from '../../assets/images/planet.svg'; +import {LectureCard} from "../../components/containers/LectureCard/LectureCard"; + +import exampleimg from '../../assets/images/photos/example_flowers.png' export const HomeView: React.FC = () => { @@ -16,6 +19,14 @@ export const HomeView: React.FC = () => {

Hello universe!

+ {user?.nickname}

Login Page diff --git a/webpack.config.js b/webpack.config.js index fa2d7ca..9e72cac 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -27,7 +27,10 @@ module.exports = { { loader: 'css-loader', options: { - modules: true, + modules: { + localIdentName: "[local]__[name]#[hash:base64:2]", + exportLocalsConvention: "camelCase", + } }, }, ], From ce53d34b1e227f228631f906c95047e47530fae0 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Thu, 24 Mar 2022 15:35:47 +0100 Subject: [PATCH 05/16] Move App component to src --- src/{components => }/App.test.tsx | 0 src/{components => }/App.tsx | 4 ++-- src/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{components => }/App.test.tsx (100%) rename src/{components => }/App.tsx (60%) diff --git a/src/components/App.test.tsx b/src/App.test.tsx similarity index 100% rename from src/components/App.test.tsx rename to src/App.test.tsx diff --git a/src/components/App.tsx b/src/App.tsx similarity index 60% rename from src/components/App.tsx rename to src/App.tsx index 5bf0721..4d2c72a 100644 --- a/src/components/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { AuthContextProvider } from '../common/auth/AuthContext'; -import { AppRouter } from '../views/AppRouter'; +import { AuthContextProvider } from './common/auth/AuthContext'; +import { AppRouter } from './views/AppRouter'; export const App: React.FC = () => { diff --git a/src/index.tsx b/src/index.tsx index 3ec350c..af2cf64 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from 'react-dom'; import {ApolloClient, ApolloProvider, InMemoryCache} from "@apollo/client"; -import { App } from './components/App'; +import { App } from './App'; const client = new ApolloClient({ uri: 'https://48p1r2roz4.sse.codesandbox.io', From 6976f1361f653c893c47df6f7dddeb985956366a Mon Sep 17 00:00:00 2001 From: DiD3n Date: Fri, 25 Mar 2022 14:00:28 +0100 Subject: [PATCH 06/16] Cleanup --- src/App.tsx | 14 +++++++-- src/common/lectures/LecturesContext.tsx | 42 ------------------------- src/common/lectures/service/Lectures.ts | 14 --------- src/common/{ => utils}/hooks/useMap.ts | 0 src/index.tsx | 11 +------ 5 files changed, 12 insertions(+), 69 deletions(-) delete mode 100644 src/common/lectures/LecturesContext.tsx delete mode 100644 src/common/lectures/service/Lectures.ts rename src/common/{ => utils}/hooks/useMap.ts (100%) diff --git a/src/App.tsx b/src/App.tsx index 4d2c72a..a9dac57 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,20 @@ import React from 'react'; import { AuthContextProvider } from './common/auth/AuthContext'; import { AppRouter } from './views/AppRouter'; +import {ApolloClient, ApolloProvider, InMemoryCache} from "@apollo/client"; + +const client = new ApolloClient({ + uri: 'https://48p1r2roz4.sse.codesandbox.io', + cache: new InMemoryCache() +}); export const App: React.FC = () => { return ( - - - + + + + + ); }; diff --git a/src/common/lectures/LecturesContext.tsx b/src/common/lectures/LecturesContext.tsx deleted file mode 100644 index edb24d8..0000000 --- a/src/common/lectures/LecturesContext.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, {createContext} from 'react'; -import {Lecture, SimpleLecture} from "../models/Lecture.model"; -import {fetchLectureById} from "./service/Lectures"; -import axios from "axios"; -import {NetworkResource, ResourceStatus} from "../NetworkResource"; - -interface LecturesContextInterface { - lectures: LecturesContextInterface[] - - get(id: string): NetworkResource - getPage(page: number, limit: number): NetworkResource -} -const defaultLectureContext: LecturesContextInterface = { - lectures: [], - - get: () => ResourceStatus.Error, - getPage: () => ResourceStatus.Error, -} - -export const LecturesContext = createContext(defaultLectureContext); - -export const LectureProvider: React.FC = ({/* axios ,*/ children}) => { - - - const getLecture = (id: string): NetworkResource => { - - if (id) { - fetchLectureById(axios, id) - .catch(console.error) - } - - return ResourceStatus.Pending; - } - - return ( - - {children} - - ) -} \ No newline at end of file diff --git a/src/common/lectures/service/Lectures.ts b/src/common/lectures/service/Lectures.ts deleted file mode 100644 index 5153517..0000000 --- a/src/common/lectures/service/Lectures.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Axios, {AxiosInstance} from 'axios'; -import {Lecture, SimpleLecture} from "../../models/Lecture.model"; - -export const fetchLecturesList = async (axios: AxiosInstance, page: number, limit: number) => - axios.get('/api/v1/lectures', { - params: { - page, limit - } - }) - .then(res => res.data); - -export const fetchLectureById = async (axios: AxiosInstance, id: string) => - axios.get(`/api/v1/lecture/${id}`) - .then(res => res.data); diff --git a/src/common/hooks/useMap.ts b/src/common/utils/hooks/useMap.ts similarity index 100% rename from src/common/hooks/useMap.ts rename to src/common/utils/hooks/useMap.ts diff --git a/src/index.tsx b/src/index.tsx index af2cf64..9b8f82f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,20 +1,11 @@ import React from 'react'; import { render } from 'react-dom'; -import {ApolloClient, ApolloProvider, InMemoryCache} from "@apollo/client"; import { App } from './App'; -const client = new ApolloClient({ - uri: 'https://48p1r2roz4.sse.codesandbox.io', - cache: new InMemoryCache() -}); - - import './index.css'; render( - - - , + , document.getElementById('root') ); From 93f8d1909bbd49685e8f2bea5df30db957a68f8a Mon Sep 17 00:00:00 2001 From: DiD3n Date: Fri, 25 Mar 2022 14:44:36 +0100 Subject: [PATCH 07/16] Add test img --- src/assets/images/photos/test_img1.jpg | Bin 0 -> 60785 bytes .../containers/LectureCard/LectureCard.css | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/assets/images/photos/test_img1.jpg diff --git a/src/assets/images/photos/test_img1.jpg b/src/assets/images/photos/test_img1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2deae207549ca8d535fb34944269fe81c86a9a4d GIT binary patch literal 60785 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 Date: Fri, 25 Mar 2022 14:46:01 +0100 Subject: [PATCH 08/16] Move apollo & client factory to separate files --- src/App.tsx | 14 ++------- src/common/auth/AuthContext.tsx | 53 ++++++++++++++++++-------------- src/common/auth/apolloService.ts | 12 ++++++++ src/common/auth/axiosService.ts | 10 ++++++ src/views/home/index.tsx | 2 +- 5 files changed, 56 insertions(+), 35 deletions(-) create mode 100644 src/common/auth/apolloService.ts create mode 100644 src/common/auth/axiosService.ts diff --git a/src/App.tsx b/src/App.tsx index a9dac57..4d2c72a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,12 @@ import React from 'react'; import { AuthContextProvider } from './common/auth/AuthContext'; import { AppRouter } from './views/AppRouter'; -import {ApolloClient, ApolloProvider, InMemoryCache} from "@apollo/client"; - -const client = new ApolloClient({ - uri: 'https://48p1r2roz4.sse.codesandbox.io', - cache: new InMemoryCache() -}); export const App: React.FC = () => { return ( - - - - - + + + ); }; diff --git a/src/common/auth/AuthContext.tsx b/src/common/auth/AuthContext.tsx index 4c05090..af552c2 100644 --- a/src/common/auth/AuthContext.tsx +++ b/src/common/auth/AuthContext.tsx @@ -1,4 +1,4 @@ -import axios, { AxiosInstance } from "axios"; +import { AxiosInstance } from "axios"; import jwtDecode from "jwt-decode"; import React, {createContext, useEffect, useState} from "react"; import Providers from "../models/ProvidersList"; @@ -6,6 +6,9 @@ import { User } from "../models/User"; import { AuthResponse } from "./service/authResponse.interface"; import { authByProvidersToken, refreshToken as refreshApiToken } from "./service/Auth"; import { JwtData } from "./service/jwtData.interface"; +import { createAxiosClient } from "./axiosService"; +import {ApolloClient, ApolloProvider, InMemoryCache, NormalizedCacheObject} from "@apollo/client"; +import {createApolloClient} from "./apolloService"; export interface AuthContextInterface { @@ -24,13 +27,9 @@ const defaultAuthContext: AuthContextInterface = { export const AuthContext = createContext(defaultAuthContext); - -const AXIOS_CONFIG = { - baseURL: process.env.API_URL -} - export const AuthContextProvider: React.FC = ({ children }) => { - const [axiosInstance, setAxiosInstance] = useState(() => axios.create(AXIOS_CONFIG)); + const [axiosClient, setAxiosClient] = useState(() => createAxiosClient()); + const [apolloClient, setApolloClient] = useState>(() => createApolloClient()); const [user, setUser] = useState(null); @@ -39,7 +38,13 @@ export const AuthContextProvider: React.FC = ({ children }) => { * @param authRes auth request's response */ const handleAuth = (authRes: AuthResponse): User => { - axiosInstance.defaults.headers.common["Authorization"] = authRes.accessToken; + axiosClient.defaults.headers.common["Authorization"] = authRes.accessToken; + setApolloClient(() => createApolloClient({ + cache: new InMemoryCache(), + headers: { + ["Authorization"]: authRes.accessToken + } + })) localStorage.setItem("refresh_token", authRes.refreshToken); @@ -58,7 +63,7 @@ export const AuthContextProvider: React.FC = ({ children }) => { // setup interceptor for 401 useEffect(() => { - const _axiosInstance = axiosInstance; // React 17.0.0 C: + const _axiosInstance = axiosClient; // React 17.0.0 C: const interceptorId = _axiosInstance.interceptors.response.use(response => response, async (error) => { const code = error.response ? error.response.status : null; @@ -70,7 +75,7 @@ export const AuthContextProvider: React.FC = ({ children }) => { // clear to prevent endless loop // next line will override refresh_token anyway localStorage.removeItem("refresh_token"); - const authRes = await refreshApiToken(axiosInstance, refresh_token) + const authRes = await refreshApiToken(axiosClient, refresh_token) handleAuth(authRes); }) @@ -79,7 +84,7 @@ export const AuthContextProvider: React.FC = ({ children }) => { // now sure if gc will handle this 'in time' _axiosInstance.interceptors.response.eject(interceptorId); } - }, [axiosInstance]); + }, [axiosClient]); // get token at app start useEffect(() => { @@ -87,7 +92,7 @@ export const AuthContextProvider: React.FC = ({ children }) => { if (refresh_token && refresh_token != "undefined") - refreshApiToken(axiosInstance, refresh_token) + refreshApiToken(axiosClient, refresh_token) .then(handleAuth) .catch((err) => { console.error(err) @@ -99,7 +104,7 @@ export const AuthContextProvider: React.FC = ({ children }) => { }, []) const auth = async (provider: Providers, code: string, state?: string): Promise => { - const authRes = await authByProvidersToken(axiosInstance, provider, code, state); + const authRes = await authByProvidersToken(axiosClient, provider, code, state); if (!authRes) throw new Error("Auth failed!"); @@ -108,20 +113,22 @@ export const AuthContextProvider: React.FC = ({ children }) => { } const logout = () => { - setAxiosInstance(() => axios.create(AXIOS_CONFIG)); + setAxiosClient(() => createAxiosClient()); setUser(() => null); localStorage.setItem("refresh_token", ""); } return ( - - {children} - + + + {children} + + ) }; diff --git a/src/common/auth/apolloService.ts b/src/common/auth/apolloService.ts new file mode 100644 index 0000000..cefdd18 --- /dev/null +++ b/src/common/auth/apolloService.ts @@ -0,0 +1,12 @@ +import {ApolloClient, ApolloClientOptions, InMemoryCache, NormalizedCacheObject} from "@apollo/client"; + +const APOLLO_CONFIG: ApolloClientOptions = { + uri: `${process.env.API_URL}/graphql`, + cache: new InMemoryCache(), +} + +export const createApolloClient = ( + apolloConfig?: ApolloClientOptions +): ApolloClient => { + return new ApolloClient({...apolloConfig, ...APOLLO_CONFIG}); +} diff --git a/src/common/auth/axiosService.ts b/src/common/auth/axiosService.ts new file mode 100644 index 0000000..216366b --- /dev/null +++ b/src/common/auth/axiosService.ts @@ -0,0 +1,10 @@ +import axios, {AxiosInstance, AxiosRequestConfig} from "axios"; + +const AXIOS_CONFIG: AxiosRequestConfig = { + baseURL: process.env.API_URL +} + +export const createAxiosClient = ( + axiosConfig?: AxiosRequestConfig): AxiosInstance => { + return axios.create({ ...axiosConfig, ...AXIOS_CONFIG}); +} \ No newline at end of file diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx index e1f2150..cfddf4a 100644 --- a/src/views/home/index.tsx +++ b/src/views/home/index.tsx @@ -6,7 +6,7 @@ import { useAuth } from '../../common/auth/useAuth.hook'; import Planet from '../../assets/images/planet.svg'; import {LectureCard} from "../../components/containers/LectureCard/LectureCard"; -import exampleimg from '../../assets/images/photos/example_flowers.png' +import exampleimg from '../../assets/images/photos/test_img1.jpg' export const HomeView: React.FC = () => { From dd943c044a0d57d6611679852ab19170c66a9e7d Mon Sep 17 00:00:00 2001 From: DiD3n Date: Wed, 6 Apr 2022 20:23:15 +0200 Subject: [PATCH 09/16] Add NavBar & Logo component --- src/assets/brand/logo.svg | 14 ++++++++++ src/assets/brand/logotype.svg | 7 +++++ src/assets/brand/mark.svg | 9 +++++++ src/components/ui/AppLogo/AppLogo.module.scss | 25 +++++++++++++++++ src/components/ui/AppLogo/AppLogo.tsx | 27 +++++++++++++++++++ src/components/ui/NavBar/NavBar.module.css | 11 ++++++++ src/components/ui/NavBar/NavBar.tsx | 15 +++++++++++ 7 files changed, 108 insertions(+) create mode 100644 src/assets/brand/logo.svg create mode 100644 src/assets/brand/logotype.svg create mode 100644 src/assets/brand/mark.svg create mode 100644 src/components/ui/AppLogo/AppLogo.module.scss create mode 100644 src/components/ui/AppLogo/AppLogo.tsx create mode 100644 src/components/ui/NavBar/NavBar.module.css create mode 100644 src/components/ui/NavBar/NavBar.tsx diff --git a/src/assets/brand/logo.svg b/src/assets/brand/logo.svg new file mode 100644 index 0000000..0da364a --- /dev/null +++ b/src/assets/brand/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/brand/logotype.svg b/src/assets/brand/logotype.svg new file mode 100644 index 0000000..8c68c6d --- /dev/null +++ b/src/assets/brand/logotype.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/brand/mark.svg b/src/assets/brand/mark.svg new file mode 100644 index 0000000..d16fd7e --- /dev/null +++ b/src/assets/brand/mark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/ui/AppLogo/AppLogo.module.scss b/src/components/ui/AppLogo/AppLogo.module.scss new file mode 100644 index 0000000..fb1aa86 --- /dev/null +++ b/src/components/ui/AppLogo/AppLogo.module.scss @@ -0,0 +1,25 @@ + +.logo { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 10px; + + // mods + &.invert { + color: white; + } + + &.vertical { + flex-direction: column; + gap: 6px; + + .mark { + width: 64px; + height: 64px; + } + } +} + + diff --git a/src/components/ui/AppLogo/AppLogo.tsx b/src/components/ui/AppLogo/AppLogo.tsx new file mode 100644 index 0000000..97d45da --- /dev/null +++ b/src/components/ui/AppLogo/AppLogo.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import cs from 'classnames'; + +import Mark from '../../../assets/brand/mark.svg'; +import Logotype from '../../../assets/brand/logotype.svg'; + +import styles from './AppLogo.module.scss'; + +interface AppLogoProps { + vertical?: boolean + invert?: boolean +} + +export const AppLogo: React.FC = ({ vertical, invert }) => { + + const styleMods = { + [styles.vertical]: vertical, + [styles.invert]: invert, + } + + return ( + + + + + ); +} \ No newline at end of file diff --git a/src/components/ui/NavBar/NavBar.module.css b/src/components/ui/NavBar/NavBar.module.css new file mode 100644 index 0000000..a6fc5e1 --- /dev/null +++ b/src/components/ui/NavBar/NavBar.module.css @@ -0,0 +1,11 @@ + +.container { + /* limit width usable space */ + max-width: var(--app-prefer-width); + margin: auto; + + /* center content */ + padding: 16px; + display: flex; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/components/ui/NavBar/NavBar.tsx b/src/components/ui/NavBar/NavBar.tsx new file mode 100644 index 0000000..1f07f1b --- /dev/null +++ b/src/components/ui/NavBar/NavBar.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import styles from './NavBar.module.css'; +import {AppLogo} from "../AppLogo/AppLogo"; + +export const NavBar: React.FC = () => { + + return ( +
+ + + burger +
+ ); +}; \ No newline at end of file From afd11676c455a7bfbea64f58499dd702ffcf389c Mon Sep 17 00:00:00 2001 From: DiD3n Date: Sat, 9 Apr 2022 13:18:38 +0200 Subject: [PATCH 10/16] Push code --- package-lock.json | 36 +++- package.json | 3 +- src/App.tsx | 2 +- src/api/graphql/EventQuery.interface.ts | 0 .../queries => api/graphql}/schema.graphql | 184 ++++++++++-------- .../auth/service => api/rest/auth}/Auth.ts | 2 +- src/api/rest/auth/AuthResponse.interface.ts | 18 ++ .../rest/auth/DecodedJwt.interface.ts} | 4 +- src/api/rest/user/User.ts | 10 + src/api/rest/user/UserResponse.interface.ts | 14 ++ src/assets/images/icons/burger.svg | 5 + src/common/auth/AuthContext.tsx | 138 ------------- src/common/auth/apolloService.ts | 12 -- .../auth/service/authResponse.interface.ts | 10 - src/common/auth/useAuth.hook.ts | 5 - src/common/models/Lecture.model.ts | 5 +- src/common/models/User.ts | 25 ++- ...eCard.module.css => EventCard.module.scss} | 4 +- .../{LectureCard.tsx => EventCard.tsx} | 12 +- src/components/ui/EventList/EventList.tsx | 32 +-- src/components/ui/NavBar/NavBar.module.css | 8 + src/components/ui/NavBar/NavBar.tsx | 27 ++- .../utils/MenuBase/MenuBase.module.scss | 49 +++++ src/components/utils/MenuBase/MenuBase.tsx | 53 +++++ .../utils/requireAuth/RequireAuth.tsx | 2 +- .../__tests__/RequireAuth.test.tsx | 4 +- src/contexts/auth/AuthContext.tsx | 109 +++++++++++ .../auth/__test__/BasicAuthorization.test.tsx | 2 +- .../auth/__test__/authResponse.json | 0 src/contexts/auth/apolloService.ts | 33 ++++ src/{common => contexts}/auth/axiosService.ts | 0 src/contexts/auth/hooks/useAuth.hook.ts | 5 + src/index.css | 2 + src/views/AppRouter.tsx | 9 +- src/views/auth/callback/github/index.tsx | 2 +- .../callback/githubdev/GithubDevCallback.tsx | 2 +- src/views/event/EventView.tsx | 17 ++ src/views/home/HomeView.module.scss | 10 + src/views/home/HomeView.tsx | 28 +++ src/views/home/index.tsx | 37 ---- vite.config.ts | 9 +- 41 files changed, 591 insertions(+), 338 deletions(-) create mode 100644 src/api/graphql/EventQuery.interface.ts rename src/{common/queries => api/graphql}/schema.graphql (60%) rename src/{common/auth/service => api/rest/auth}/Auth.ts (85%) create mode 100644 src/api/rest/auth/AuthResponse.interface.ts rename src/{common/auth/service/jwtData.interface.ts => api/rest/auth/DecodedJwt.interface.ts} (67%) create mode 100644 src/api/rest/user/User.ts create mode 100644 src/api/rest/user/UserResponse.interface.ts create mode 100644 src/assets/images/icons/burger.svg delete mode 100644 src/common/auth/AuthContext.tsx delete mode 100644 src/common/auth/apolloService.ts delete mode 100644 src/common/auth/service/authResponse.interface.ts delete mode 100644 src/common/auth/useAuth.hook.ts rename src/components/containers/LectureCard/{LectureCard.module.css => EventCard.module.scss} (88%) rename src/components/containers/LectureCard/{LectureCard.tsx => EventCard.tsx} (87%) create mode 100644 src/components/utils/MenuBase/MenuBase.module.scss create mode 100644 src/components/utils/MenuBase/MenuBase.tsx create mode 100644 src/contexts/auth/AuthContext.tsx rename src/{common => contexts}/auth/__test__/BasicAuthorization.test.tsx (98%) rename src/{common => contexts}/auth/__test__/authResponse.json (100%) create mode 100644 src/contexts/auth/apolloService.ts rename src/{common => contexts}/auth/axiosService.ts (100%) create mode 100644 src/contexts/auth/hooks/useAuth.hook.ts create mode 100644 src/views/event/EventView.tsx create mode 100644 src/views/home/HomeView.module.scss create mode 100644 src/views/home/HomeView.tsx delete mode 100644 src/views/home/index.tsx diff --git a/package-lock.json b/package-lock.json index 14b9ebf..680cd81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "0.1.2", "license": "ISC", "dependencies": { - "@apollo/client": "^3.5.9", - "@babel/runtime": "^7.15.4", + "@apollo/client": "^3.5.10", "@vitejs/plugin-react": "^1.2.0", "express": "^4.17.1", "express-history-api-fallback": "^2.2.1", @@ -28,6 +27,7 @@ "@typescript-eslint/eslint-plugin": "^4.31.2", "@typescript-eslint/parser": "^4.31.2", "axios": "^0.23.0", + "classnames": "^2.3.1", "eslint": "^7.32.0", "eslint-plugin-react": "^7.26.0", "formik": "^2.2.9", @@ -590,6 +590,7 @@ "version": "7.15.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -3217,6 +3218,12 @@ "dev": true, "peer": true }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", + "dev": true + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -8830,9 +8837,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/minimist-options": { "version": "4.1.0", @@ -9815,7 +9822,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, "node_modules/regexp.prototype.flags": { "version": "1.3.1", @@ -11980,6 +11988,7 @@ "version": "7.15.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -13945,6 +13954,12 @@ "dev": true, "peer": true }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", + "dev": true + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -18113,9 +18128,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minimist-options": { "version": "4.1.0", @@ -18854,7 +18869,8 @@ "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, "regexp.prototype.flags": { "version": "1.3.1", diff --git a/package.json b/package.json index 0506854..ba0c505 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@typescript-eslint/eslint-plugin": "^4.31.2", "@typescript-eslint/parser": "^4.31.2", "axios": "^0.23.0", + "classnames": "^2.3.1", "eslint": "^7.32.0", "eslint-plugin-react": "^7.26.0", "formik": "^2.2.9", @@ -101,7 +102,7 @@ "typescript": "^4.4.3" }, "dependencies": { - "@apollo/client": "^3.5.9", + "@apollo/client": "^3.5.10", "@vitejs/plugin-react": "^1.2.0", "express": "^4.17.1", "express-history-api-fallback": "^2.2.1", diff --git a/src/App.tsx b/src/App.tsx index 4d2c72a..ac19136 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { AuthContextProvider } from './common/auth/AuthContext'; +import { AuthContextProvider } from './contexts/auth/AuthContext'; import { AppRouter } from './views/AppRouter'; export const App: React.FC = () => { diff --git a/src/api/graphql/EventQuery.interface.ts b/src/api/graphql/EventQuery.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/common/queries/schema.graphql b/src/api/graphql/schema.graphql similarity index 60% rename from src/common/queries/schema.graphql rename to src/api/graphql/schema.graphql index 155b3aa..c7b145a 100644 --- a/src/common/queries/schema.graphql +++ b/src/api/graphql/schema.graphql @@ -1,7 +1,7 @@ - -schema { - query: Query - mutation: Mutation +input AddressRequestInput { + city: String! + coordinates: CoordinatesRequestInput + place: String! } type AddressResponse { @@ -10,6 +10,13 @@ type AddressResponse { place: String! } +input ContactRequestInput { + github: String + linkedin: String + mail: String + twitter: String +} + type ContactResponse { github: String linkedin: String @@ -17,119 +24,126 @@ type ContactResponse { twitter: String } +input CoordinatesRequestInput { + latitude: Float! + longitude: Float! +} + type CoordinatesResponse { latitude: Float! longitude: Float! } -type LectureResponse { +input EventRequestInput { + address: AddressRequestInput + subtitle: String + tags: [TagRequestInput!]! + timeFrame: TimeFrameRequestInput! + title: String! +} + +type EventResponse { address: AddressResponse - "Lecture creator" - author: UserResponse! authorId: String! createDate: Instant! id: String! - "Is lecture followed by current user" - isFollowed: Boolean! subtitle: String tags: [TagResponse!]! + theme: ThemeResponse! timeFrame: TimeFrameResponse! title: String! updateDate: Instant! + + # Event creator + author: UserResponse! + + # Is event followed by current user + isFollowed: Boolean! +} + +input EventThemeRequestInput { + primaryColor: String + secondaryColor: String } +# ISO date-time +scalar Instant + type Mutation { - "Create lecture with request" - createLecture(request: LectureRequestInput!): LectureResponse! - "Creates tag" + # Creates tag createTag(request: TagCreateRequestInput!): TagResponse! - "Delete lecture by id" - deleteLecture(id: String!): Boolean! - "Follow Lecture by id" - followLecture(id: String!): Boolean! - "Appends new tags to followed list" + + # Appends new tags to followed list followTags(request: [TagRequestInput!]!): [TagResponse!]! - "Replace lecture data with new data (PUT equivalent)" - replaceLecture(id: String!, request: LectureRequestInput!): LectureResponse! - "Replace user data with new data (PUT equivalent)" - replaceMyUser(request: UserRequestInput!): UserResponse! - "Unfollow lecture by id" - unfollowLecture(id: String!): Boolean! - "Removes tags from followed list" + + # Removes tags from followed list unfollowTags(request: [TagRequestInput!]!): Boolean! + + # Create event with request + createEvent(request: EventRequestInput!): EventResponse! + + # Delete event by id + deleteEvent(id: String!): Boolean! + + # Follow event by id + followEvent(id: String!): Boolean! + + # Replace event data with new data (PUT equivalent) + replaceEvent(id: String!, request: EventRequestInput!): EventResponse! + + # Replace event theme with new one (PUT equivalent) + replaceEventTheme( + id: String! + request: EventThemeRequestInput! + ): EventResponse! + + # Unfollow event by id + unfollowEvent(id: String!): Boolean! + + # Replace user data with new data (PUT equivalent) + replaceMyUser(request: UserRequestInput!): UserResponse! } type Query { - "Get tags followed by user (paged)" + # Get tags followed by user (paged) followedTags(page: Int, size: Int): [TagResponse!]! - "Get single lecture by its id" - lecture(id: String!): LectureResponse - "Get all lectures paged" - lectures(page: Int, size: Int): [LectureResponse!]! - "Get tag by id" - tag(id: String!): TagResponse - "Get all tags paged" - tags(page: Int, size: Int): [TagResponse!]! - "Get user by id" - user(id: String!): UserResponse - "Get all users paged" - users(page: Int, size: Int): [UserResponse!]! -} -type TagResponse { - id: String! - name: String! -} + # Get tag by id + tag(id: String!): TagResponse -type TimeFrameResponse { - finishDate: Instant - startDate: Instant! -} + # Get all tags paged + tags(page: Int, size: Int): [TagResponse!]! -type UserResponse { - contact: ContactResponse! - createDate: Instant! - description: String - id: String! - nickname: String! - updateDate: Instant! -} + # Get single event by its id + event(id: String!): EventResponse -"ISO date-time" -scalar Instant + # Get all events paged + events(page: Int, size: Int): [EventResponse!]! -input AddressRequestInput { - city: String! - coordinates: CoordinatesRequestInput - place: String! -} + # Get user by id + user(id: String!): UserResponse -input ContactRequestInput { - github: String - linkedin: String - mail: String - twitter: String + # Get all users paged + users(page: Int, size: Int): [UserResponse!]! } -input CoordinatesRequestInput { - latitude: Float! - longitude: Float! +input TagCreateRequestInput { + name: String! } -input LectureRequestInput { - address: AddressRequestInput - subtitle: String - tags: [TagRequestInput!]! - timeFrame: TimeFrameRequestInput! - title: String! +input TagRequestInput { + id: String! } -input TagCreateRequestInput { +type TagResponse { + id: String! name: String! } -input TagRequestInput { - id: String! +type ThemeResponse { + image: String + primaryColor: String + secondaryColor: String } input TimeFrameRequestInput { @@ -137,8 +151,22 @@ input TimeFrameRequestInput { startDate: Instant! } +type TimeFrameResponse { + finishDate: Instant + startDate: Instant! +} + input UserRequestInput { contact: ContactRequestInput! description: String nickname: String! } + +type UserResponse { + contact: ContactResponse! + createDate: Instant! + description: String + id: String! + nickname: String! + updateDate: Instant! +} diff --git a/src/common/auth/service/Auth.ts b/src/api/rest/auth/Auth.ts similarity index 85% rename from src/common/auth/service/Auth.ts rename to src/api/rest/auth/Auth.ts index e9165df..2dcce80 100644 --- a/src/common/auth/service/Auth.ts +++ b/src/api/rest/auth/Auth.ts @@ -1,5 +1,5 @@ import { AxiosInstance } from 'axios'; -import { AuthResponse } from './authResponse.interface'; +import { AuthResponse } from './AuthResponse.interface'; export const authByProvidersToken = async (axios: AxiosInstance, provider: string, token: string, state?: string): Promise => axios.get(`/oauth/callback/${provider}`, { diff --git a/src/api/rest/auth/AuthResponse.interface.ts b/src/api/rest/auth/AuthResponse.interface.ts new file mode 100644 index 0000000..2342be2 --- /dev/null +++ b/src/api/rest/auth/AuthResponse.interface.ts @@ -0,0 +1,18 @@ +import {UserResponse} from "../user/UserResponse.interface"; + +export interface AuthResponse { + username: string + + roles: string[] + + accessToken: string + refreshToken: string + tokenType: string + expiresIn: number + + user: UserResponse + + properties: { + isInitialized: boolean + } +} diff --git a/src/common/auth/service/jwtData.interface.ts b/src/api/rest/auth/DecodedJwt.interface.ts similarity index 67% rename from src/common/auth/service/jwtData.interface.ts rename to src/api/rest/auth/DecodedJwt.interface.ts index 58364d4..1930771 100644 --- a/src/common/auth/service/jwtData.interface.ts +++ b/src/api/rest/auth/DecodedJwt.interface.ts @@ -1,11 +1,11 @@ -export interface JwtData { +export interface DecodedJwt { id: string; nickname: string; exp: number; // expire iat: number; - iss: string; // service name + iss: string; // api name roles: string[], sub: string; } diff --git a/src/api/rest/user/User.ts b/src/api/rest/user/User.ts new file mode 100644 index 0000000..528e616 --- /dev/null +++ b/src/api/rest/user/User.ts @@ -0,0 +1,10 @@ +import {AxiosInstance} from "axios"; +import {UserResponse} from "./UserResponse.interface"; + +export const getMe = (axios: AxiosInstance): Promise => + axios.get('/api/v1/users/@me') + .then(res => res.data); + +export const getById = (axios: AxiosInstance, id: string): Promise => + axios.get(`/api/v1/users/${id}`) + .then(res => res.data); \ No newline at end of file diff --git a/src/api/rest/user/UserResponse.interface.ts b/src/api/rest/user/UserResponse.interface.ts new file mode 100644 index 0000000..e05f2c0 --- /dev/null +++ b/src/api/rest/user/UserResponse.interface.ts @@ -0,0 +1,14 @@ + +export interface UserResponse { + id: string + nickname: string + description?: string + contact: { + github?: string + linkedin?: string + mail?: string + twitter?: string + } + createDate: string + updateDate: string +} \ No newline at end of file diff --git a/src/assets/images/icons/burger.svg b/src/assets/images/icons/burger.svg new file mode 100644 index 0000000..6364abf --- /dev/null +++ b/src/assets/images/icons/burger.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/common/auth/AuthContext.tsx b/src/common/auth/AuthContext.tsx deleted file mode 100644 index e077ae6..0000000 --- a/src/common/auth/AuthContext.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { AxiosInstance } from "axios"; -import jwtDecode from "jwt-decode"; -import React, {createContext, useEffect, useState} from "react"; -import Providers from "../models/ProvidersList"; -import { User } from "../models/User"; -import { AuthResponse } from "./service/authResponse.interface"; -import { authByProvidersToken, refreshToken as refreshApiToken } from "./service/Auth"; -import { JwtData } from "./service/jwtData.interface"; -import { createAxiosClient } from "./axiosService"; -import {ApolloClient, ApolloProvider, InMemoryCache, NormalizedCacheObject} from "@apollo/client"; -import {createApolloClient} from "./apolloService"; - - -export interface AuthContextInterface { - auth(provider: string, code: string, state?: string): Promise; - logout(): void; - - user: User | null; // undefined == not authorized - - axios?: AxiosInstance; -} -const defaultAuthContext: AuthContextInterface = { - auth: async () => { throw new Error("Context not initialized") }, - logout: () => { throw new Error("Context not initialized") }, - user: null, -} - -export const AuthContext = createContext(defaultAuthContext); - -export const AuthContextProvider: React.FC = ({ children }) => { - //todo(DiD3n): move to separate (axios) context - const [axiosClient, setAxiosClient] = useState(() => createAxiosClient()); - //todo(DiD3n): move to separate (apollo) context - const [apolloClient, setApolloClient] = useState>(() => createApolloClient()); - - const [user, setUser] = useState(null); - - /** - * Update axios authorization header, decode user, save refresh_token - * @param authRes auth request's response - */ - const handleAuth = (authRes: AuthResponse): User => { - axiosClient.defaults.headers.common["Authorization"] = authRes.accessToken; - setApolloClient(() => createApolloClient({ - cache: new InMemoryCache(), - headers: { - ["Authorization"]: authRes.accessToken, - ["Access-Control-Allow-Origin"]: 'true', - }, - z - })) - - localStorage.setItem("refresh_token", authRes.refreshToken); - - const decodedToken: JwtData = jwtDecode(authRes.accessToken); - - //todo(DiD3n): convince 'backend guy' to include user in response - const user = { - id: decodedToken.id, - nickname: authRes.username, - tags: ["dev", "***** ***", "i use arch btw"] - }; - - setUser(() => user); - return user; - } - - // setup interceptor for 401 - useEffect(() => { - const _axiosInstance = axiosClient; // React 17.0.0 C: - - const interceptorId = _axiosInstance.interceptors.response.use(response => response, async (error) => { - const code = error.response ? error.response.status : null; - const refresh_token = localStorage.getItem("refresh_token"); - - if (code != 401 || !refresh_token) - return Promise.reject(error); - - // clear to prevent endless loop - // next line will override refresh_token anyway - localStorage.removeItem("refresh_token"); - const authRes = await refreshApiToken(axiosClient, refresh_token) - - handleAuth(authRes); - }) - - return () => { - // now sure if gc will handle this 'in time' - _axiosInstance.interceptors.response.eject(interceptorId); - } - }, [axiosClient]); - - // get token at app start - useEffect(() => { - const refresh_token = localStorage.getItem("refresh_token"); - - - if (refresh_token && refresh_token != "undefined") - refreshApiToken(axiosClient, refresh_token) - .then(handleAuth) - .catch((err) => { - console.error(err) - localStorage.removeItem("refresh_token"); - }) - else - localStorage.removeItem("refresh_token"); - - }, []) - - const auth = async (provider: Providers, code: string, state?: string): Promise => { - const authRes = await authByProvidersToken(axiosClient, provider, code, state); - - if (!authRes) - throw new Error("Auth failed!"); - - return handleAuth(authRes); - } - - const logout = () => { - setAxiosClient(() => createAxiosClient()); - setUser(() => null); - localStorage.setItem("refresh_token", ""); - } - - return ( - - - {children} - - - ) -}; diff --git a/src/common/auth/apolloService.ts b/src/common/auth/apolloService.ts deleted file mode 100644 index 43c3631..0000000 --- a/src/common/auth/apolloService.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {ApolloClient, ApolloClientOptions, InMemoryCache, NormalizedCacheObject} from "@apollo/client"; - -const APOLLO_CONFIG: ApolloClientOptions = { - uri: `${__GRAPHQL_URI__}/graphql`, - cache: new InMemoryCache(), -} - -export const createApolloClient = ( - apolloConfig?: ApolloClientOptions -): ApolloClient => { - return new ApolloClient({...apolloConfig, ...APOLLO_CONFIG}); -} diff --git a/src/common/auth/service/authResponse.interface.ts b/src/common/auth/service/authResponse.interface.ts deleted file mode 100644 index f20efe4..0000000 --- a/src/common/auth/service/authResponse.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ - -export interface AuthResponse { - username: string; - accessToken: string; - refreshToken: string; - - roles: string[]; - expiresIn: number; - tokenType: string; -} diff --git a/src/common/auth/useAuth.hook.ts b/src/common/auth/useAuth.hook.ts deleted file mode 100644 index b644278..0000000 --- a/src/common/auth/useAuth.hook.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useContext } from "react"; -import { AuthContext, AuthContextInterface } from "./AuthContext"; - -export const useAuth = (): AuthContextInterface => - useContext(AuthContext); \ No newline at end of file diff --git a/src/common/models/Lecture.model.ts b/src/common/models/Lecture.model.ts index feb4322..65d725e 100644 --- a/src/common/models/Lecture.model.ts +++ b/src/common/models/Lecture.model.ts @@ -7,6 +7,7 @@ export class SimpleLecture { public name: string, public description: string, public startDate: string, + public primaryColor: string, ) { } } @@ -23,13 +24,15 @@ export class Lecture extends SimpleLecture { public latitude: number, public longitude: number, public agenda: AgendaEntry[] = [], + public primaryColor: string, public author?: User, ) { super( id, name, description, - startDate + startDate, + primaryColor, ); } } diff --git a/src/common/models/User.ts b/src/common/models/User.ts index 5490ae8..c853bff 100644 --- a/src/common/models/User.ts +++ b/src/common/models/User.ts @@ -1,11 +1,28 @@ +import {UserResponse} from "../../api/rest/user/UserResponse.interface"; export interface SimpleUser { id: string, nickname: string, } -export interface User { - id: string, - nickname: string, - tags: string[], +export class User implements SimpleUser { + + constructor( + public id: string, + public nickname: string, + public description: string, + public createDate: Date, + public updateDate: Date, + ) { + } + + static fromResponse(userResponse: UserResponse): User { + return new User( + userResponse.id, + userResponse.nickname, + userResponse.description || "", + new Date(userResponse.createDate), + new Date(userResponse.updateDate), + ) + } } diff --git a/src/components/containers/LectureCard/LectureCard.module.css b/src/components/containers/LectureCard/EventCard.module.scss similarity index 88% rename from src/components/containers/LectureCard/LectureCard.module.css rename to src/components/containers/LectureCard/EventCard.module.scss index 620085e..7a811c4 100644 --- a/src/components/containers/LectureCard/LectureCard.module.css +++ b/src/components/containers/LectureCard/EventCard.module.scss @@ -8,7 +8,7 @@ box-sizing: border-box; position: relative; - margin: 8px; + margin: 8px 0; min-height: 113px; padding: var(--padding) var(--padding); @@ -25,7 +25,7 @@ border-radius: 16px; opacity: .4; - -webkit-mask-image: linear-gradient(90deg, #FFF 50%, transparent); + -webkit-mask-image: -webkit-gradient(linear, 50% top, right top, from(#FFF), to(transparent)); mask-image: linear-gradient(90deg, #FFF 50%, transparent); } .title { diff --git a/src/components/containers/LectureCard/LectureCard.tsx b/src/components/containers/LectureCard/EventCard.tsx similarity index 87% rename from src/components/containers/LectureCard/LectureCard.tsx rename to src/components/containers/LectureCard/EventCard.tsx index 62b1ab1..af39ef7 100644 --- a/src/components/containers/LectureCard/LectureCard.tsx +++ b/src/components/containers/LectureCard/EventCard.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Link } from 'react-router-dom' -import styles from './LectureCard.module.css' +import styles from './EventCard.module.scss' import {LocationChip} from "./chips/LocationChip"; import {StartDateChip} from "./chips/StartDateChip"; @@ -17,14 +17,14 @@ interface LectureCardProps { locationName?: string } -export const LectureCard: React.FC = ({ +export const EventCard: React.FC = ({ id, title, subtitle, color, image, startDate, - locationName + locationName, }) => { const cardStyle: React.CSSProperties = { @@ -32,10 +32,12 @@ export const LectureCard: React.FC = ({ } return ( - +
+ {image && } +

{title} @@ -43,8 +45,8 @@ export const LectureCard: React.FC = ({

{subtitle}

-

+
{locationName && {locationName}} diff --git a/src/components/ui/EventList/EventList.tsx b/src/components/ui/EventList/EventList.tsx index 1e81cb2..cb7edd8 100644 --- a/src/components/ui/EventList/EventList.tsx +++ b/src/components/ui/EventList/EventList.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { gql, useQuery} from "@apollo/client"; -import {LectureCard} from "../../containers/LectureCard/LectureCard"; +import {EventCard} from "../../containers/LectureCard/EventCard"; import exampleimg from "../../../assets/images/photos/test_img1.jpg"; -const GET_LECTURES = gql` +const GET_EVENTS = gql` query getLectures($page: Int, $size: Int) { - lectures(page: $page, size: $size) { + events(page: $page, size: $size) { id title subtitle @@ -15,16 +15,20 @@ const GET_LECTURES = gql` timeFrame { startDate } + theme { + primaryColor + image + } } } `; export const EventList: React.FC = () => { - const {loading, error, data} = useQuery(GET_LECTURES, { + const {loading, error, data} = useQuery(GET_EVENTS, { variables: { page: 1, - size: 5, + size: 50, } }); @@ -33,16 +37,16 @@ export const EventList: React.FC = () => { return (
- {data && data.lectures.map((lecture: any) => - //todo: convert to Model + )} -
) } \ No newline at end of file diff --git a/src/components/ui/NavBar/NavBar.module.css b/src/components/ui/NavBar/NavBar.module.css index a6fc5e1..e3007d8 100644 --- a/src/components/ui/NavBar/NavBar.module.css +++ b/src/components/ui/NavBar/NavBar.module.css @@ -8,4 +8,12 @@ padding: 16px; display: flex; justify-content: space-between; + align-items: center; +} + +.burgerButton { + margin: 0; + padding: 0; + border: none; + background-color: unset; } \ No newline at end of file diff --git a/src/components/ui/NavBar/NavBar.tsx b/src/components/ui/NavBar/NavBar.tsx index 1f07f1b..c0a5807 100644 --- a/src/components/ui/NavBar/NavBar.tsx +++ b/src/components/ui/NavBar/NavBar.tsx @@ -1,15 +1,36 @@ -import React from 'react'; +import React, {useState} from 'react'; import styles from './NavBar.module.css'; import {AppLogo} from "../AppLogo/AppLogo"; +import {Link} from "react-router-dom"; +import {MenuBase} from "../../utils/MenuBase/MenuBase"; +import {NavMenu} from "../NavMenu/NavMenu"; + +import Burger from '/assets/images/icons/burger.svg'; export const NavBar: React.FC = () => { + const [open, setOpen] = useState(false); + return (
- + + + - burger + + + + + +
); }; \ No newline at end of file diff --git a/src/components/utils/MenuBase/MenuBase.module.scss b/src/components/utils/MenuBase/MenuBase.module.scss new file mode 100644 index 0000000..41f0a66 --- /dev/null +++ b/src/components/utils/MenuBase/MenuBase.module.scss @@ -0,0 +1,49 @@ +$local-z-index: 100; + +.base { + text-align: center; + font-size: 0; +} + +// hook's purpose is only to change +// `display: absolute`' pos' coords to local one +.hook { + position: relative; + + .content { + + // detach from DOM's layout + position: absolute; + z-index: $local-z-index + 1; + // bc of hook we don't need to calc global pos + top: 0; + left: 0; + + // mods: + &.horizontal-left { + transform: translateX(-100%); + } + &.horizontal-center { + transform: translateX(-50%); + } + &.horizontal-right { + transform: translateX(0%); + } + } +} + + +.overlay { + position: absolute; + top: 0; + left: 0; + + width: 100vw; + height: 100vh; + + z-index: $local-z-index; + + &.visible { + background-color: rgba(0, 0, 0, 0.1); + } +} diff --git a/src/components/utils/MenuBase/MenuBase.tsx b/src/components/utils/MenuBase/MenuBase.tsx new file mode 100644 index 0000000..c7bd996 --- /dev/null +++ b/src/components/utils/MenuBase/MenuBase.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import cs from 'classnames'; + +import styles from './MenuBase.module.scss'; + +interface MenuBaseProp { + isOpen: boolean + setOpen: React.Dispatch + position?: "left" | "center" | "right" + overlay?: boolean, +} + +/** + * @description + * Universal wrapper for menus with build-in logic + * @example + * const [open, setOpen] = useState(false); + * + * - -

- - ); -}; diff --git a/vite.config.ts b/vite.config.ts index 5aeb75e..77c0629 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,17 +14,12 @@ export default defineConfig(({command, /* mode */}) => { }, server: { port: 8080, - proxy: { - '/graphql': { - target: "http://jeteo.newbies.pl:8080" - } - } }, define: { "__RESTAPI_URI__": "'http://jeteo.newbies.pl:8080'", - "__GRAPHQL_URI__": command === "serve" ? "'http://localhost:8080'" : "'http://jeteo.newbies.pl:8080'", + "__GRAPHQL_URI__": "'http://jeteo.newbies.pl:8080'", "__DEV__": command === "serve", }, } -}) +}); From c87156e93c9105f6070bea21d8b8f0080495f92d Mon Sep 17 00:00:00 2001 From: DiD3n Date: Sat, 9 Apr 2022 13:19:12 +0200 Subject: [PATCH 11/16] Move NavMenu to ui --- src/components/ui/NavMenu/NavMenu.module.scss | 41 ++++++++++++++++ src/components/ui/NavMenu/NavMenu.tsx | 48 +++++++++++++++++++ .../NavMenuItem/NavMenuItem.module.scss | 24 ++++++++++ .../NavMenu/NavMenuItem/NavMenuItemButton.tsx | 16 +++++++ .../NavMenu/NavMenuItem/NavMenuItemLink.tsx | 17 +++++++ 5 files changed, 146 insertions(+) create mode 100644 src/components/ui/NavMenu/NavMenu.module.scss create mode 100644 src/components/ui/NavMenu/NavMenu.tsx create mode 100644 src/components/ui/NavMenu/NavMenuItem/NavMenuItem.module.scss create mode 100644 src/components/ui/NavMenu/NavMenuItem/NavMenuItemButton.tsx create mode 100644 src/components/ui/NavMenu/NavMenuItem/NavMenuItemLink.tsx diff --git a/src/components/ui/NavMenu/NavMenu.module.scss b/src/components/ui/NavMenu/NavMenu.module.scss new file mode 100644 index 0000000..486ac46 --- /dev/null +++ b/src/components/ui/NavMenu/NavMenu.module.scss @@ -0,0 +1,41 @@ + +.container { + width: 200px; + background-color: #135465; + border-radius: var(--border-radius); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25); + margin: 8px; +} + +.userInfo { + padding: 14px 14px 10px; + text-align: center; + + color: #fff; + + .username { + font-weight: 700; + font-size: 22px; + margin: 4px; + } + + .role { + font-weight: 400; + font-size: 16px; + margin: 0; + } +} + +.innerContainer { + padding: 14px; + background-color: #fff; + text-align: center; + border-radius: var(--border-radius); +} + +.separator { + margin: 2px 0; + height: 2px; + background-color: transparentize(#DBDCDD, 0.1); + border: none; +} \ No newline at end of file diff --git a/src/components/ui/NavMenu/NavMenu.tsx b/src/components/ui/NavMenu/NavMenu.tsx new file mode 100644 index 0000000..870fc28 --- /dev/null +++ b/src/components/ui/NavMenu/NavMenu.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import styles from './NavMenu.module.scss'; +import {useAuth} from "../../../contexts/auth/hooks/useAuth.hook"; +import {NavMenuItemLink} from "./NavMenuItem/NavMenuItemLink"; +import {NavMenuItemButton} from "./NavMenuItem/NavMenuItemButton"; + +export const NavMenu: React.FC = ({ }) => { + + const {user, logout} = useAuth(); + + return ( +
+ +
+

{user?.nickname}

+

Użytkownik

+
+ +
+ + + jeteo™ studio + + + + ustawienia + + +
+ + {user ? + + wyloguj + + : + + zaloguj + + } +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/NavMenu/NavMenuItem/NavMenuItem.module.scss b/src/components/ui/NavMenu/NavMenuItem/NavMenuItem.module.scss new file mode 100644 index 0000000..abec1bb --- /dev/null +++ b/src/components/ui/NavMenu/NavMenuItem/NavMenuItem.module.scss @@ -0,0 +1,24 @@ +.item { + display: block; + + margin: 8px 0; + width: 100%; + + background-color: transparent; + border: none; + + color: #000; + text-decoration: none; + + text-align: center; + font-weight: 400; + font-size: 18px; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } +} \ No newline at end of file diff --git a/src/components/ui/NavMenu/NavMenuItem/NavMenuItemButton.tsx b/src/components/ui/NavMenu/NavMenuItem/NavMenuItemButton.tsx new file mode 100644 index 0000000..bc706c1 --- /dev/null +++ b/src/components/ui/NavMenu/NavMenuItem/NavMenuItemButton.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +import styles from './NavMenuItem.module.scss' + +interface NavMenuItemButtonProps { + onClick: () => void +} + +export const NavMenuItemButton: React.FC = ({ + onClick, + children, +}) => ( + +) diff --git a/src/components/ui/NavMenu/NavMenuItem/NavMenuItemLink.tsx b/src/components/ui/NavMenu/NavMenuItem/NavMenuItemLink.tsx new file mode 100644 index 0000000..3e60d44 --- /dev/null +++ b/src/components/ui/NavMenu/NavMenuItem/NavMenuItemLink.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import {Link} from "react-router-dom" + +import styles from './NavMenuItem.module.scss' + +interface NavMenuItemLinkProps { + location: string +} + +export const NavMenuItemLink: React.FC = ({ + location, + children, +}) => ( + + {children} + +) From c8adbe97debc50c0191abe1bf32368a8c9eb6e5c Mon Sep 17 00:00:00 2001 From: DiD3n Date: Mon, 11 Apr 2022 20:15:17 +0200 Subject: [PATCH 12/16] Fix stuff --- src/api/graphql/EventQuery.interface.ts | 0 src/api/graphql/events/EventDataQuery.ts | 38 ++++++++++++ src/api/graphql/events/EventListQuery.ts | 31 ++++++++++ src/common/models/Lecture.model.ts | 38 ------------ src/common/models/SimpleEvent.model.ts | 36 +++++++++++ .../LectureCard/EventCard.module.scss | 2 +- .../containers/LectureCard/EventCard.tsx | 51 +++++++-------- src/components/ui/EventList/EventList.tsx | 62 +++++++++---------- src/views/AppRouter.tsx | 2 +- src/views/event/EventView.tsx | 11 +++- 10 files changed, 167 insertions(+), 104 deletions(-) delete mode 100644 src/api/graphql/EventQuery.interface.ts create mode 100644 src/api/graphql/events/EventDataQuery.ts create mode 100644 src/api/graphql/events/EventListQuery.ts delete mode 100644 src/common/models/Lecture.model.ts create mode 100644 src/common/models/SimpleEvent.model.ts diff --git a/src/api/graphql/EventQuery.interface.ts b/src/api/graphql/EventQuery.interface.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/graphql/events/EventDataQuery.ts b/src/api/graphql/events/EventDataQuery.ts new file mode 100644 index 0000000..77b14fb --- /dev/null +++ b/src/api/graphql/events/EventDataQuery.ts @@ -0,0 +1,38 @@ +import {gql} from "@apollo/client"; + +export const GET_EVENT_QUERY = gql` + query getEvent($id: String!) { + event(id: $id) { + id + title + subtitle + author { + nickname + } + timeFrame { + startDate + } + theme { + primaryColor + image + } + } + } +`; + +export interface EventData { + id: string + title: string + subtitle: string + author: { + id: string + nickname: string + } + timeFrame: { + startDate: string + } + theme: { + primaryColor: string + image: string + } +} diff --git a/src/api/graphql/events/EventListQuery.ts b/src/api/graphql/events/EventListQuery.ts new file mode 100644 index 0000000..468b0d7 --- /dev/null +++ b/src/api/graphql/events/EventListQuery.ts @@ -0,0 +1,31 @@ +import {gql} from "@apollo/client"; +import {EventData} from "./EventDataQuery"; + +export const GET_EVENTS_LIST_QUERY = gql` + query getEventsList($page: Int, $size: Int) { + events(page: $page, size: $size) { + id + title + subtitle + author { + nickname + } + timeFrame { + startDate + } + theme { + primaryColor + image + } + } + } +`; + +export interface EventListQueryVars { + page: number, + size: number, +} + +export interface EventListQueryData { + events: EventData[] +} diff --git a/src/common/models/Lecture.model.ts b/src/common/models/Lecture.model.ts deleted file mode 100644 index 65d725e..0000000 --- a/src/common/models/Lecture.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {AgendaEntry} from "./Agenda.model"; -import {User} from "./User"; - -export class SimpleLecture { - constructor( - public id: string, - public name: string, - public description: string, - public startDate: string, - public primaryColor: string, - ) { - } -} - -export class Lecture extends SimpleLecture { - constructor( - public id: string, - public name: string, - public description: string, - public startDate: string, - public finishDate: string, - public city: string, - public place: string, - public latitude: number, - public longitude: number, - public agenda: AgendaEntry[] = [], - public primaryColor: string, - public author?: User, - ) { - super( - id, - name, - description, - startDate, - primaryColor, - ); - } -} diff --git a/src/common/models/SimpleEvent.model.ts b/src/common/models/SimpleEvent.model.ts new file mode 100644 index 0000000..629c0e1 --- /dev/null +++ b/src/common/models/SimpleEvent.model.ts @@ -0,0 +1,36 @@ +import {SimpleUser} from "./User"; +import {EventData} from "../../api/graphql/events/EventDataQuery"; + + +export class Event { + constructor( + public id: string, + public title: string, + public subtitle: string, + public author: SimpleUser, + public startDate: Date, + public primaryColor: string, + public image: string, + ) { + } + + get vanityUrl(): string { + const name = this.title + .toLowerCase() + .replaceAll(/[ \-_/?&<>=+]/mg, '-'); + + return [name, this.id].join('-') + } + + static fromData(data: EventData): Event { + return new Event( + data.id, + data.title, + data.subtitle, + data.author as SimpleUser, + new Date(data.timeFrame.startDate), + data.theme.primaryColor, + data.theme.image + ) + } +} \ No newline at end of file diff --git a/src/components/containers/LectureCard/EventCard.module.scss b/src/components/containers/LectureCard/EventCard.module.scss index 7a811c4..3bf5526 100644 --- a/src/components/containers/LectureCard/EventCard.module.scss +++ b/src/components/containers/LectureCard/EventCard.module.scss @@ -10,7 +10,7 @@ margin: 8px 0; min-height: 113px; - padding: var(--padding) var(--padding); + padding: var(--padding) var(--padding) 48px; border-radius: 16px; box-shadow: 0 2px 24px rgba(7, 16, 24, 0.3); diff --git a/src/components/containers/LectureCard/EventCard.tsx b/src/components/containers/LectureCard/EventCard.tsx index af39ef7..e129f60 100644 --- a/src/components/containers/LectureCard/EventCard.tsx +++ b/src/components/containers/LectureCard/EventCard.tsx @@ -1,11 +1,9 @@ import React from 'react' -import { Link } from 'react-router-dom' - import styles from './EventCard.module.scss' import {LocationChip} from "./chips/LocationChip"; import {StartDateChip} from "./chips/StartDateChip"; -interface LectureCardProps { +interface EventCardProps { id: string, title: string, subtitle: string, @@ -17,8 +15,7 @@ interface LectureCardProps { locationName?: string } -export const EventCard: React.FC = ({ - id, +export const EventCard: React.FC = ({ title, subtitle, color, @@ -32,29 +29,27 @@ export const EventCard: React.FC = ({ } return ( - -
- - {image && - } - -
-

- {title} -

-

- {subtitle} -

-
- -
- {locationName && - {locationName}} - - {startDate && - } -
+
+ + {image && + } + +
+

+ {title} +

+

+ {subtitle} +

+
+ +
+ {locationName && + {locationName}} + + {startDate && + }
- +
) } \ No newline at end of file diff --git a/src/components/ui/EventList/EventList.tsx b/src/components/ui/EventList/EventList.tsx index cb7edd8..aada5e5 100644 --- a/src/components/ui/EventList/EventList.tsx +++ b/src/components/ui/EventList/EventList.tsx @@ -1,31 +1,20 @@ import React from 'react'; -import { gql, useQuery} from "@apollo/client"; -import {EventCard} from "../../containers/LectureCard/EventCard"; +import { useQuery } from "@apollo/client"; +import { EventCard } from "../../containers/LectureCard/EventCard"; import exampleimg from "../../../assets/images/photos/test_img1.jpg"; +import { + EventListQueryData, + EventListQueryVars, + GET_EVENTS_LIST_QUERY +} from "../../../api/graphql/events/EventListQuery"; +import {Event} from "../../../common/models/SimpleEvent.model"; +import {Link} from "react-router-dom"; -const GET_EVENTS = gql` - query getLectures($page: Int, $size: Int) { - events(page: $page, size: $size) { - id - title - subtitle - author { - nickname - } - timeFrame { - startDate - } - theme { - primaryColor - image - } - } - } -`; export const EventList: React.FC = () => { - const {loading, error, data} = useQuery(GET_EVENTS, { + const {loading, error, data} = useQuery( + GET_EVENTS_LIST_QUERY, { variables: { page: 1, size: 50, @@ -33,20 +22,27 @@ export const EventList: React.FC = () => { }); if (loading) return <>loading...; - if (error) return <>error
{error.message}; + if (error) return

error
{error.message}

; return (
- {data && data.events.map((event: any) => //todo: convert to Model - )} + {data && data.events + .map(Event.fromData) + .map(event => + + + + + )}
) } \ No newline at end of file diff --git a/src/views/AppRouter.tsx b/src/views/AppRouter.tsx index fb93f15..4b9d56f 100644 --- a/src/views/AppRouter.tsx +++ b/src/views/AppRouter.tsx @@ -25,7 +25,7 @@ export const AppRouter: React.FC = () => { } - path="event/:id"/> + path="event/:name"/> diff --git a/src/views/event/EventView.tsx b/src/views/event/EventView.tsx index 0600d7f..991a273 100644 --- a/src/views/event/EventView.tsx +++ b/src/views/event/EventView.tsx @@ -1,17 +1,22 @@ import React from 'react'; -import { useParams } from "react-router-dom"; + +import { useParams, Navigate } from "react-router-dom"; import { NavBar } from "../../components/ui/NavBar/NavBar"; export const EventView: React.FC = () => { + const { name } = useParams<{name: string}>(); + + if (!name) + return ; - const { id } = useParams<{id: string}>(); + const id = name?.match(/[a-zA-Z0-9_]+$/)?.at(0); return ( <> - {id} + {id ? id : 'idk'} ) } From 564ca4c9e2fbb3c3565d28593e880fbc0719cc43 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Mon, 11 Apr 2022 20:17:30 +0200 Subject: [PATCH 13/16] Fix lint --- src/common/utils/hooks/useMap.ts | 12 ------------ src/components/ui/NavMenu/NavMenu.tsx | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 src/common/utils/hooks/useMap.ts diff --git a/src/common/utils/hooks/useMap.ts b/src/common/utils/hooks/useMap.ts deleted file mode 100644 index a2f367f..0000000 --- a/src/common/utils/hooks/useMap.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useState } from "react"; - -export const useMap = () => { - const [store, setStore] = useState>(new Map()); - - return { - get: (key: K) => store.get(key), - has: (key: K) => store.has(key), - set: (key: K, val: T) => - setStore( store => store.set(key, val)), - } -} \ No newline at end of file diff --git a/src/components/ui/NavMenu/NavMenu.tsx b/src/components/ui/NavMenu/NavMenu.tsx index 870fc28..b39111e 100644 --- a/src/components/ui/NavMenu/NavMenu.tsx +++ b/src/components/ui/NavMenu/NavMenu.tsx @@ -5,7 +5,7 @@ import {useAuth} from "../../../contexts/auth/hooks/useAuth.hook"; import {NavMenuItemLink} from "./NavMenuItem/NavMenuItemLink"; import {NavMenuItemButton} from "./NavMenuItem/NavMenuItemButton"; -export const NavMenu: React.FC = ({ }) => { +export const NavMenu: React.FC = () => { const {user, logout} = useAuth(); From 7a4a7ed8cb12e3ef38d0e5c4f2d3c09d72c7a890 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Mon, 11 Apr 2022 20:28:39 +0200 Subject: [PATCH 14/16] Fix tests --- package.json | 5 ++-- .../requireAuth/__tests__/authResponse.json | 23 ++++++++++++++++--- src/contexts/auth/__test__/authResponse.json | 23 ++++++++++++++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ba0c505..52badec 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,9 @@ "^.+\\.tsx?$": "ts-jest" }, "globals": { - "__API_URL__": "http://localhost/", - "__DEV__": true + "__RESTAPI_URI__": "'http://localhost:8080'", + "__GRAPHQL_URI__": "'http://localhost:8080'", + "__DEV__": false }, "moduleNameMapper": { "\\.s?[ca]ss$": "/src/__mocks__/css.ts", diff --git a/src/components/utils/requireAuth/__tests__/authResponse.json b/src/components/utils/requireAuth/__tests__/authResponse.json index 52a217e..e1287cd 100644 --- a/src/components/utils/requireAuth/__tests__/authResponse.json +++ b/src/components/utils/requireAuth/__tests__/authResponse.json @@ -1,7 +1,24 @@ { "username": "test-user", + "roles": [], "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjIxMzciLCJuaWNrbmFtZSI6InRlc3QtdXNlciJ9.60uv_s_0VmHPcwhxCtR8w2bIu2EooskFkzzHJjsbvT4", "refreshToken": "cashuidbdyuiawvydiwpdawvyidvbyidwbyui", - "roles": [], - "expiresIn": 32193021839120831290830932123123132 -} \ No newline at end of file + "tokenType": "BestJWT", + "expiresIn": 32193021839120831290830932123123132, + "user": { + "id": "uSeR1D", + "nickname": "test-user", + "description": "", + "contact": { + "github": null, + "linkedin": null, + "mail": null, + "twitter": null + }, + "createDate": "2022-04-11T18:26:38.275Z", + "updateDate": "2022-04-11T18:26:38.275Z" + }, + "properties": { + "isInitialized": true + } +} diff --git a/src/contexts/auth/__test__/authResponse.json b/src/contexts/auth/__test__/authResponse.json index 52a217e..e1287cd 100644 --- a/src/contexts/auth/__test__/authResponse.json +++ b/src/contexts/auth/__test__/authResponse.json @@ -1,7 +1,24 @@ { "username": "test-user", + "roles": [], "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjIxMzciLCJuaWNrbmFtZSI6InRlc3QtdXNlciJ9.60uv_s_0VmHPcwhxCtR8w2bIu2EooskFkzzHJjsbvT4", "refreshToken": "cashuidbdyuiawvydiwpdawvyidvbyidwbyui", - "roles": [], - "expiresIn": 32193021839120831290830932123123132 -} \ No newline at end of file + "tokenType": "BestJWT", + "expiresIn": 32193021839120831290830932123123132, + "user": { + "id": "uSeR1D", + "nickname": "test-user", + "description": "", + "contact": { + "github": null, + "linkedin": null, + "mail": null, + "twitter": null + }, + "createDate": "2022-04-11T18:26:38.275Z", + "updateDate": "2022-04-11T18:26:38.275Z" + }, + "properties": { + "isInitialized": true + } +} From de582067f823fbaaa1282d2c6c2d56e3384b1085 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Tue, 12 Apr 2022 20:19:09 +0200 Subject: [PATCH 15/16] Move Providers to api/rest --- .../rest/auth/oauth/Provider.ts} | 4 +-- src/common/models/Agenda.model.ts | 8 ----- .../models/{SimpleEvent.model.ts => Event.ts} | 0 src/common/models/Provider.ts | 6 ---- src/components/ui/EventList/EventList.tsx | 2 +- src/components/ui/NavMenu/NavMenu.tsx | 34 +++++++++++-------- src/contexts/auth/AuthContext.tsx | 2 +- src/views/auth/callback/github/index.tsx | 2 +- .../callback/githubdev/GithubDevCallback.tsx | 2 +- 9 files changed, 25 insertions(+), 35 deletions(-) rename src/{common/models/ProvidersList.ts => api/rest/auth/oauth/Provider.ts} (52%) delete mode 100644 src/common/models/Agenda.model.ts rename src/common/models/{SimpleEvent.model.ts => Event.ts} (100%) delete mode 100644 src/common/models/Provider.ts diff --git a/src/common/models/ProvidersList.ts b/src/api/rest/auth/oauth/Provider.ts similarity index 52% rename from src/common/models/ProvidersList.ts rename to src/api/rest/auth/oauth/Provider.ts index 72708c8..ac3f4f4 100644 --- a/src/common/models/ProvidersList.ts +++ b/src/api/rest/auth/oauth/Provider.ts @@ -1,7 +1,7 @@ -enum ProvidersList { +enum Provider { github = 'github', githubDev = 'devgithub' } -export default ProvidersList; \ No newline at end of file +export default Provider; \ No newline at end of file diff --git a/src/common/models/Agenda.model.ts b/src/common/models/Agenda.model.ts deleted file mode 100644 index c8fe561..0000000 --- a/src/common/models/Agenda.model.ts +++ /dev/null @@ -1,8 +0,0 @@ - -export interface AgendaEntry { - name: string - description?: string - - startDate: Date - endDate: Date -} \ No newline at end of file diff --git a/src/common/models/SimpleEvent.model.ts b/src/common/models/Event.ts similarity index 100% rename from src/common/models/SimpleEvent.model.ts rename to src/common/models/Event.ts diff --git a/src/common/models/Provider.ts b/src/common/models/Provider.ts deleted file mode 100644 index 3aca90a..0000000 --- a/src/common/models/Provider.ts +++ /dev/null @@ -1,6 +0,0 @@ - -export interface Provider { - name: string, - logo: string, - url: string, -} diff --git a/src/components/ui/EventList/EventList.tsx b/src/components/ui/EventList/EventList.tsx index aada5e5..0e44fbb 100644 --- a/src/components/ui/EventList/EventList.tsx +++ b/src/components/ui/EventList/EventList.tsx @@ -7,7 +7,7 @@ import { EventListQueryVars, GET_EVENTS_LIST_QUERY } from "../../../api/graphql/events/EventListQuery"; -import {Event} from "../../../common/models/SimpleEvent.model"; +import {Event} from "../../../common/models/Event"; import {Link} from "react-router-dom"; diff --git a/src/components/ui/NavMenu/NavMenu.tsx b/src/components/ui/NavMenu/NavMenu.tsx index b39111e..4b9b87b 100644 --- a/src/components/ui/NavMenu/NavMenu.tsx +++ b/src/components/ui/NavMenu/NavMenu.tsx @@ -12,24 +12,28 @@ export const NavMenu: React.FC = () => { return (
-
-

{user?.nickname}

-

Użytkownik

-
+ {user && +
+

{user?.nickname}

+

Użytkownik

+
+ }
- - jeteo™ studio - - - - ustawienia - - -
+ {user && + <> + + jeteo™ studio + + + ustawienia + +
+ + } {user ? { const { auth, user } = useAuth(); From 4816067bfe5037d60dc1fc7355258d9687089f48 Mon Sep 17 00:00:00 2001 From: DiD3n Date: Wed, 13 Apr 2022 16:34:16 +0200 Subject: [PATCH 16/16] Fixes --- .../containers/LectureCard/EventCard.tsx | 5 ++++- src/contexts/auth/AuthContext.tsx | 15 +++++++-------- .../auth/__test__/BasicAuthorization.test.tsx | 8 ++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/containers/LectureCard/EventCard.tsx b/src/components/containers/LectureCard/EventCard.tsx index e129f60..fd76c69 100644 --- a/src/components/containers/LectureCard/EventCard.tsx +++ b/src/components/containers/LectureCard/EventCard.tsx @@ -25,7 +25,7 @@ export const EventCard: React.FC = ({ }) => { const cardStyle: React.CSSProperties = { - backgroundColor: color || "#080736" //space dark + backgroundColor: color } return ( @@ -52,4 +52,7 @@ export const EventCard: React.FC = ({
) +} +EventCard.defaultProps = { + color: "#080736" } \ No newline at end of file diff --git a/src/contexts/auth/AuthContext.tsx b/src/contexts/auth/AuthContext.tsx index 144d8ae..a3b35f8 100644 --- a/src/contexts/auth/AuthContext.tsx +++ b/src/contexts/auth/AuthContext.tsx @@ -9,15 +9,14 @@ import Providers from "../../api/rest/auth/oauth/Provider"; import { User } from "../../common/models/User"; export interface AuthContextInterface { - auth(provider: string, code: string, state?: string): Promise; - logout(): void; - user: User | null; // undefined == not authorized - refreshSession(): void; - axios: AxiosInstance, + auth(provider: string, code: string, state?: string): Promise + logout(): void + user?: User + refreshSession(): void + axios: AxiosInstance } export const AuthContext = createContext({ - user: null, auth: async () => { throw new Error("AuthContext not initialized") }, logout: () => { throw new Error("AuthContext not initialized") }, refreshSession: () => { throw new Error("AuthContext not initialized") }, @@ -25,7 +24,7 @@ export const AuthContext = createContext({ }); export const AuthContextProvider: React.FC = ({ children }) => { - const [user, setUser] = useState(null); + const [user, setUser] = useState(undefined); const [axiosClient, setAxiosClient] = useState(() => createAxiosClient()); const [apolloClient, setApolloClient] = useState>(() => createApolloClient()); @@ -67,7 +66,7 @@ export const AuthContextProvider: React.FC = ({ children }) => { setApolloClient(() => createApolloClient()); setAxiosClient(() => createAxiosClient()); - setUser(() => null); + setUser(() => undefined); localStorage.setItem("refresh_token", ""); } diff --git a/src/contexts/auth/__test__/BasicAuthorization.test.tsx b/src/contexts/auth/__test__/BasicAuthorization.test.tsx index e654589..1aca243 100644 --- a/src/contexts/auth/__test__/BasicAuthorization.test.tsx +++ b/src/contexts/auth/__test__/BasicAuthorization.test.tsx @@ -67,7 +67,7 @@ describe('Authorization Context', () => { }) it('should login', async () => { - expect(testContext?.user).toBe(null); + expect(testContext?.user).toBe(undefined); act(() => { userEvent.click(screen.getByTestId('login'), { button: 0 }) @@ -82,7 +82,7 @@ describe('Authorization Context', () => { }) it('should login & logout', async () => { - expect(testContext?.user).toBe(null); + expect(testContext?.user).toBe(undefined); act(() => { userEvent.click(screen.getByTestId('login'), { button: 0 }) @@ -91,13 +91,13 @@ describe('Authorization Context', () => { await waitFor(() => screen.getByTestId('id')); - expect(testContext?.user).not.toBe(null); + expect(testContext?.user).toBeDefined(); expect(testContext?.user?.nickname).toBe(authRes.username); act(() => { userEvent.click(screen.getByTestId('logout'), { button: 0 }); }) - expect(testContext?.user).toBe(null); + expect(testContext?.user).toBe(undefined); }) }) \ No newline at end of file