diff --git a/package-lock.json b/package-lock.json index 4684cc2..a424a57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-hot-toast": "^2.2.0", + "react-modal": "^3.14.4", "sass": "^1.42.1", "superjson": "^1.8.0", "swr": "^1.1.2" @@ -32,6 +33,8 @@ "@testing-library/react": "^12.1.1", "@types/faker": "5.5.9", "@types/jest": "^27.0.2", + "@types/qrcode.react": "^1.0.2", + "@types/react-modal": "^3.13.1", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", @@ -2126,6 +2129,15 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, + "node_modules/@types/qrcode.react": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-1.0.2.tgz", + "integrity": "sha512-I9Oq5Cjlkgy3Tw7krCnCXLw2/zMhizkTere49OOcta23tkvH0xBTP0yInimTh0gstLRtb8Ki9NZVujE5UI6ffQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react": { "version": "17.0.37", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", @@ -2136,6 +2148,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -4806,6 +4827,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -9420,6 +9446,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.14.4.tgz", + "integrity": "sha512-8surmulejafYCH9wfUmFyj4UfbSJwjcgbS9gf3oOItu4Hwd6ivJyVBETI0yHRhpJKCLZMUtnhzk76wXTsNL6Qg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17" + } + }, "node_modules/react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -10818,6 +10867,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.0.tgz", @@ -12634,6 +12691,15 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, + "@types/qrcode.react": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-1.0.2.tgz", + "integrity": "sha512-I9Oq5Cjlkgy3Tw7krCnCXLw2/zMhizkTere49OOcta23tkvH0xBTP0yInimTh0gstLRtb8Ki9NZVujE5UI6ffQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react": { "version": "17.0.37", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", @@ -12644,6 +12710,15 @@ "csstype": "^3.0.2" } }, + "@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -14681,6 +14756,11 @@ "strip-final-newline": "^2.0.0" } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -18080,6 +18160,22 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-modal": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.14.4.tgz", + "integrity": "sha512-8surmulejafYCH9wfUmFyj4UfbSJwjcgbS9gf3oOItu4Hwd6ivJyVBETI0yHRhpJKCLZMUtnhzk76wXTsNL6Qg==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -19148,6 +19244,14 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.0.tgz", diff --git a/package.json b/package.json index e02e306..defb462 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-hot-toast": "^2.2.0", + "react-modal": "^3.14.4", "sass": "^1.42.1", "superjson": "^1.8.0", "swr": "^1.1.2" @@ -37,6 +38,8 @@ "@testing-library/react": "^12.1.1", "@types/faker": "5.5.9", "@types/jest": "^27.0.2", + "@types/qrcode.react": "^1.0.2", + "@types/react-modal": "^3.13.1", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", diff --git a/src/components/App/DisplayPuzzleInstances/DisplayPuzzleInstances.module.scss b/src/components/App/DisplayPuzzleInstances/DisplayPuzzleInstances.module.scss new file mode 100644 index 0000000..10b3132 --- /dev/null +++ b/src/components/App/DisplayPuzzleInstances/DisplayPuzzleInstances.module.scss @@ -0,0 +1,88 @@ +@import '../../../styles/abstracts/variables'; +@import '../../../styles/abstracts/_mixins/text.scss'; + +.puzzleInstancesWrapper { + display: flex; + flex-direction: column; + + .puzzleInstancesHeader { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 2rem; + + .puzzleInstancesHeaderSelectWrapper { + @include font-button; + color: $black; + display: inline-block; + overflow: hidden; + border: 2px solid $grey; + box-sizing: border-box; + border-radius: 999px; + flex: 1; + padding: 0.5rem 1rem; + position: relative; + margin-right: 2rem; + + .puzzleInstancesHeaderSelect { + cursor: pointer; + font-size: 0.875rem; + width: 120%; + height: 120%; + outline: none; + border: 0; + + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + } + &::after { + content: '▼'; + font-size: 0.8rem; + pointer-events: none; + position: absolute; + right: 1rem; + top: 0.75rem; + } + } + } + + .puzzleInstancesBody { + .puzzleInstancesBodyTitle { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + text-align: center; + } + .puzzleInstancesBodyContent { + overflow-y: auto; + height: 60vh; + .puzzleInstancesBodyContentRow { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + text-align: center; + margin-bottom: 1rem; + & > :first-child { + text-align: left; + } + align-items: center; + + .puzzleInstancesBodyContentQR { + width: 10rem; + padding: 0; + margin: 0; + } + + .puzzleInstancesBodyContentActions { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + gap: 0.5rem; + } + } + } + } +} diff --git a/src/components/App/DisplayPuzzleInstances/index.tsx b/src/components/App/DisplayPuzzleInstances/index.tsx new file mode 100644 index 0000000..bbae03d --- /dev/null +++ b/src/components/App/DisplayPuzzleInstances/index.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import styles from './DisplayPuzzleInstances.module.scss'; +import { PuzzleCustom } from '../../../types/api/puzzles/puzzle'; +import { getAllPuzzleInstances } from '../../../services'; +import { PuzzleInstance } from '@prisma/client'; +import { Button } from '../../Global'; +import { QRGenerator } from '../../App'; +import toast from 'react-hot-toast'; +import { getLinkToPuzzleInstance } from '../../../utils/getLinkToPuzzleInstance'; + +type DisplayPuzzleInstancesProps = { + puzzlesList: PuzzleCustom[]; +}; + +const DisplayPuzzleInstances = ({ + puzzlesList +}: DisplayPuzzleInstancesProps) => { + const [puzzleId, setPuzzleId] = useState(''); + const [allPuzzleInstances, setAllPuzzleInstances] = useState< + PuzzleInstance[] + >([]); + const [puzzleInstances, setPuzzleInstances] = useState([]); + + const getPuzzleInstances = async () => { + setAllPuzzleInstances(await getAllPuzzleInstances()); + }; + + useEffect(() => { + getPuzzleInstances(); + }, [puzzlesList]); + + useEffect(() => { + if (!allPuzzleInstances || !setPuzzleInstances) return; + + if (!puzzleId) return setPuzzleInstances(allPuzzleInstances); + + setPuzzleInstances( + allPuzzleInstances.filter( + instance => instance.puzzleId.toString() === puzzleId + ) + ); + }, [puzzleId, allPuzzleInstances, setPuzzleInstances]); + + return ( +
+ {/* HEADER */} +
+
+

See Locations

+
+ {/* Select puzzle */} +
+ {puzzlesList ? ( + + ) : ( + + )} +
+
+ + {/* BODY */} +
+ {/* 3 cols table title */} +
+

Address

+

QR Code

+

Actions

+
+ + {/* 3 cols table content */} +
+ {puzzleInstances && + puzzleInstances.map( + (puzzleInstance: PuzzleInstance, index: number) => ( +
+
{puzzleInstance.address}
+
+ +
+
+
+
+ ) + )} +
+
+
+ ); +}; + +export default DisplayPuzzleInstances; diff --git a/src/components/App/LocationSearchModal/LocationSearchModal.module.scss b/src/components/App/LocationSearchModal/LocationSearchModal.module.scss new file mode 100644 index 0000000..661c974 --- /dev/null +++ b/src/components/App/LocationSearchModal/LocationSearchModal.module.scss @@ -0,0 +1,20 @@ +.modalWraper { + display: flex; + flex-direction: column; + gap: 1rem; + height: 40rem; + width: 40rem; + + .reviewSubmit { + display: flex; + flex-direction: row; + gap: 1rem; + + & > :first-child { + width: 100%; + & > * { + text-align: center; + } + } + } +} diff --git a/src/components/App/LocationSearchModal/index.tsx b/src/components/App/LocationSearchModal/index.tsx new file mode 100644 index 0000000..5d89814 --- /dev/null +++ b/src/components/App/LocationSearchModal/index.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import toast from 'react-hot-toast'; +import { Geocoder } from '@maptiler/geocoder'; +import styles from './LocationSearchModal.module.scss'; +import MapGeocoder from '../MapGeocoder'; +import type { MapAnchor, MapMarker } from '../../../types/map'; +import MapRenderer from '../MapRenderer'; +import { Button, Input } from '../../Global'; + +const MAPTILER_ACCESS_TOKEN = process.env.NEXT_PUBLIC_MAPTILER_ACCESS_TOKEN; +const CENTRE_KELOWNA_LATNG: MapAnchor = [49.88307, -119.48568]; + +type LocationSearchModalProps = { + address: string; + latitude: string; + longtitude: string; + setAddress: React.Dispatch>; + setLatitude: React.Dispatch>; + setLongitude: React.Dispatch>; + setModalIsOpen: React.Dispatch>; +}; + +const LocationSearchModal = ({ + address, + latitude, + longtitude, + setAddress, + setLatitude, + setLongitude, + setModalIsOpen +}: LocationSearchModalProps) => { + const [userMarker, setUserMarker] = useState(null); + const [mapCenter, setMapCenter] = useState(null); + const [tempMarker, setTempMarker] = useState(null); + + useEffect((): void => { + navigator.geolocation.getCurrentPosition( + position => { + const anchor: MapAnchor = [ + position.coords.latitude, + position.coords.longitude + ]; + const zoom = 16; + setUserMarker({ anchor, zoom }); + setMapCenter(anchor); + }, + () => { + const anchor: MapAnchor = CENTRE_KELOWNA_LATNG; + setMapCenter(anchor); + } + ); + }, []); + + useEffect((): void => { + if (!latitude || !longtitude || !userMarker || !setTempMarker) return; + + setTempMarker({ + anchor: [parseFloat(latitude), parseFloat(longtitude)], + zoom: userMarker.zoom + }); + }, [latitude, longtitude, userMarker, setTempMarker]); + + useEffect(() => { + if (!tempMarker) return; + + const lat = tempMarker.anchor[0].toString(); + const lon = tempMarker.anchor[1].toString(); + setLatitude(lat); + setLongitude(lon); + + let geocoder = new Geocoder({ + key: MAPTILER_ACCESS_TOKEN + }); + + geocoder.geocode(`${lon},${lat}`).then(results => { + if (!results) return; + setAddress(results.features[0]['place_name']); + }); + }, [setAddress, setLatitude, setLongitude, tempMarker]); + + return ( +
+ + {userMarker && ( + + )} +
+ +
+
+ ); +}; + +export default LocationSearchModal; diff --git a/src/components/App/MapRenderer/index.tsx b/src/components/App/MapRenderer/index.tsx index 96da5ac..80c3b75 100644 --- a/src/components/App/MapRenderer/index.tsx +++ b/src/components/App/MapRenderer/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Map, Marker, ZoomControl } from 'pigeon-maps'; import { maptiler } from 'pigeon-maps/providers'; import mapRendererStyles from './MapRenderer.module.scss'; -import { MapMarker, MapRendererProps } from '../../../types/map'; +import { MapAnchor, MapMarker, MapRendererProps } from '../../../types/map'; const MAPTILER_ACCESS_TOKEN = process.env.NEXT_PUBLIC_MAPTILER_ACCESS_TOKEN; @@ -12,12 +12,19 @@ const MapRenderer = ({ markers, userMarker, mapCenter, - setMapCenter + setMapCenter, + tempMarker, + setTempMarker }: MapRendererProps) => { const setMapFocus = (marker: MapMarker): void => { setMapCenter(marker.anchor); }; + const handleOnClickMap = (latLng: MapAnchor) => { + if (!setTempMarker) return; + setTempMarker({ anchor: latLng, zoom: userMarker.zoom }); + }; + return (
{ setMapCenter(center); }} + onClick={({ event, latLng, pixel }) => { + handleOnClickMap(latLng); + }} > {markers.map((marker, index) => ( setMapFocus(userMarker)} /> )} + {tempMarker && ( + setMapFocus(tempMarker)} + /> + )}
diff --git a/src/components/App/PuzzleGenerate/PuzzleGenerate.module.scss b/src/components/App/PuzzleGenerate/PuzzleGenerate.module.scss index a3033cc..5469dbb 100644 --- a/src/components/App/PuzzleGenerate/PuzzleGenerate.module.scss +++ b/src/components/App/PuzzleGenerate/PuzzleGenerate.module.scss @@ -1,16 +1,15 @@ @import '../../../styles/abstracts/_mixins/text.scss'; - +@import '../../../styles/abstracts/variables'; .form { display: flex; flex-direction: column; - margin: 0 2rem; - gap: 1rem; + gap: 0.5rem; & > input { @include font-body; } - & > h2 { + & > h3 { margin: 0; } @@ -24,11 +23,53 @@ } } - .selections { - width: 50%; - height: 120%; - cursor: pointer; - @include font-body; + .selectionWrapper { + @include font-button; + color: $black; + display: inline-block; + overflow: hidden; + border: 2px solid #4b4b4b; + box-sizing: border-box; + border-radius: 999px; + width: 30rem; + flex: 1; + padding: 0.5rem 1rem; + position: relative; + + .selections { + cursor: pointer; + font-size: 0.875rem; + width: 120%; + height: 120%; + outline: none; + border: 0; + + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + } + &::after { + content: '▼'; + font-size: 0.8rem; + pointer-events: none; + position: absolute; + right: 1rem; + top: 0.75rem; + } + } + + .hintWrapper { + & > :first-child { + width: 30rem; + } + } + + .addressWrapper { + & > :first-child { + width: 23rem; + } + display: flex; + gap: 1rem; } .qrCode { @@ -37,3 +78,25 @@ justify-content: center; } } + +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba($grey, 0.8); + + .modal { + top: 50%; + left: 50%; + right: auto; + bottom: auto; + transform: translate(-50%, -50%); + position: absolute; + margin-right: -50%; + padding: 2rem; + background-color: $white; + border-radius: 0.5rem; + } +} diff --git a/src/components/App/PuzzleGenerate/index.tsx b/src/components/App/PuzzleGenerate/index.tsx index d9c4cd4..1edc4da 100644 --- a/src/components/App/PuzzleGenerate/index.tsx +++ b/src/components/App/PuzzleGenerate/index.tsx @@ -2,18 +2,19 @@ import * as React from 'react'; import { useState } from 'react'; import styles from './PuzzleGenerate.module.scss'; import toast from 'react-hot-toast'; +import Modal from 'react-modal'; import { Button, Input } from '../../Global'; -import { QRGenerator } from '..'; +import { LocationSearchModal } from '..'; import { createPuzzleInstance } from '../../../services'; +import { PuzzleCustom } from '../../../types/api/puzzles/puzzle'; -const PuzzleGenerate = ({ puzzlesList }) => { +const PuzzleGenerate = ({ puzzlesList, modalIsOpen, setModalIsOpen }) => { const [hint, setHint] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [address, setAddress] = useState(''); const [puzzleId, setPuzzleId] = useState(''); - const [puzzleInstanceData, setPuzzleInstanceData] = useState(); const handleSubmit = () => { const puzzleInstancePromise = async () => { @@ -25,15 +26,13 @@ const PuzzleGenerate = ({ puzzlesList }) => { hint ); - if (puzzleInstance.error) { + if (puzzleInstance?.error) { throw new Error(puzzleInstance.message); - } else { - setPuzzleInstanceData(puzzleInstance); } }; toast.promise(puzzleInstancePromise(), { loading: 'Making your puzzle instance... ⚙️', - success: 'Success', + success: 'Successfully added new puzzle instance', error: err => err.message }); }; @@ -41,71 +40,87 @@ const PuzzleGenerate = ({ puzzlesList }) => { return ( <>
-

Make a puzzle instance

- -
- - - -
-
- {puzzlesList.length > 0 && ( +

Set New Puzzle Location

+
+ {puzzlesList.length > 0 ? ( + ) : ( + <> + + )}
+
+ +
+
+ setModalIsOpen(true)} + /> +
- {puzzleInstanceData && ( -
- {/** TODO: Create link to puzzle map page */} - {/** TODO: Make it copiable */} - -
- )}
+ { + setModalIsOpen(false); + }} + contentLabel="Set Location Modal" + overlayClassName={styles.modalOverlay} + appElement={document.getElementById('__next') as HTMLElement} + > + + ); }; diff --git a/src/components/App/PuzzleInfomation/PuzzleInfomation.module.scss b/src/components/App/PuzzleInfomation/PuzzleInfomation.module.scss new file mode 100644 index 0000000..b05bec9 --- /dev/null +++ b/src/components/App/PuzzleInfomation/PuzzleInfomation.module.scss @@ -0,0 +1,27 @@ +@import '../../../styles/abstracts/_mixins/text.scss'; + +.contentWrap { + h3 { + @include font-header; + margin: 1rem 0; + } + + .tableHeader { + // make this a grid of 5 columns + display: grid; + grid-template-columns: 2fr repeat(4, 1fr); + + h4 { + @include font-header; + margin: 0; + } + } + + .tableBody { + .tableRow { + display: grid; + grid-template-columns: 2fr repeat(4, 1fr); + margin-bottom: 0.5rem; + } + } +} diff --git a/src/components/App/PuzzleInfomation/index.tsx b/src/components/App/PuzzleInfomation/index.tsx new file mode 100644 index 0000000..7d8103c --- /dev/null +++ b/src/components/App/PuzzleInfomation/index.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { PuzzleCustom } from '../../../types/api/puzzles/puzzle'; +import styles from './PuzzleInfomation.module.scss'; + +const PuzzleInfomation = ({ puzzlesList }: { puzzlesList: PuzzleCustom[] }) => { + return ( +
+

Puzzle Infomation

+
+

Puzzle Name

+

Difficulty

+

Success Rate

+

Average Age

+

Average Range

+
+
+
+ {puzzlesList.map(puzzle => ( +
+
{puzzle.name}
+
{puzzle.difficulty}
+
+ {/* TODO: Calculate success Rate */} + N/A +
+
+ {/* TODO: Implement Age */} + N/A +
+
+ {/* TODO: Implement Age */} + N/A +
+
+ ))} +
+
+ ); +}; + +export default PuzzleInfomation; diff --git a/src/components/App/QRGenerator/index.tsx b/src/components/App/QRGenerator/index.tsx index 3fd47fe..a1c2842 100644 --- a/src/components/App/QRGenerator/index.tsx +++ b/src/components/App/QRGenerator/index.tsx @@ -2,13 +2,18 @@ import * as React from 'react'; import styles from './QRGenerator.module.scss'; import QRCode from 'qrcode.react'; -const QRGenerator = ({ text }: { text: string }) => { +const QRGenerator = ({ + text, + className +}: { + text: string; + className?: string; +}) => { return ( { const [input, setInput] = useState(''); useEffect(() => { setInputVal && setInputVal(input); }, [input, setInputVal]); + + if (value && setInputVal) + return ( + <> +
+ {labelText && ( + + )} + { + setInputVal(event.target.value); + }} + disabled={disabled} // This will be false if disabled is not passed in + onClick={onClick} + /> +
+ + ); + return ( <>
@@ -38,6 +71,8 @@ const Input = ({ onChange={event => { setInput(event.target.value); }} + disabled={disabled} // This will be false if disabled is not passed in + onClick={onClick} />
diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index ca574c8..7a72b08 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -2,12 +2,43 @@ import * as React from 'react'; import styles from '../../styles/pages/admin.module.scss'; import Router from 'next/router'; import { useSession } from 'next-auth/react'; -import { PuzzleGenerate } from '../../components/App'; +import { + DisplayPuzzleInstances, + PuzzleGenerate, + PuzzleInfomation +} from '../../components/App'; import { getAllPuzzles, isAdmin } from '../../services'; import { GetServerSideProps } from 'next'; import { Header } from '../../components/Global'; +import type { PuzzleCustom } from '../../types/api/puzzles/puzzle'; -const Admin = ({ puzzlesList }) => { +const Admin = ({ puzzlesList }: { puzzlesList: PuzzleCustom[] }) => { + const [modalIsOpen, setModalIsOpen] = React.useState(false); + return ( + <> +
+ {/** TODO: Create Header for admin page */} +

Admin Dashboard

+
+
+
+ + +
+
+ +
+
+ + + ); +}; + +const AdminValidation = ({ puzzlesList }: { puzzlesList: PuzzleCustom[] }) => { const { data: session, status } = useSession(); const [validAdmin, setValidAdmin] = React.useState(null); @@ -20,7 +51,7 @@ const Admin = ({ puzzlesList }) => { const email = session?.user?.email; React.useEffect(() => { const checkAdmin = async () => { - setValidAdmin(await isAdmin(email)); + setValidAdmin(await isAdmin({ email })); }; email && checkAdmin(); }, [email]); @@ -31,16 +62,7 @@ const Admin = ({ puzzlesList }) => { if (status === 'authenticated') { if (validAdmin) { - return ( - <> -
-

ADMIN PAGE 🤓

- {/** TODO: Create Header for admin page */} -
- -
- - ); + return ; } else { validAdmin === false && Router.push('/403'); } // This will need adjustment since it's just a prototype @@ -64,4 +86,4 @@ export const getServerSideProps: GetServerSideProps = async context => { }; }; -export default Admin; +export default AdminValidation; diff --git a/src/pages/puzzles/map/index.tsx b/src/pages/puzzles/map/index.tsx index 05d6ef9..66e868b 100644 --- a/src/pages/puzzles/map/index.tsx +++ b/src/pages/puzzles/map/index.tsx @@ -5,7 +5,7 @@ import puzzleMapStyles from '../../../styles/pages/PuzzleMap.module.scss'; import { Filter, Header } from '../../../components/Global'; import { getAllPuzzleInstances } from '../../../services/puzzleInstance'; import { PuzzleMapProps } from '../../../types/puzzle'; -import { MapAnchor, MapMarker } from '../../../types/map'; +import type { MapAnchor, MapMarker } from '../../../types/map'; const PuzzleMap = ({ puzzleInstances }: PuzzleMapProps) => { const [userMarker, setUserMarker] = useState(null); diff --git a/src/services/index.ts b/src/services/index.ts index 92ada21..0b81aee 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,6 +2,7 @@ import { signUp } from './signup'; import { resetPassword } from './resetPassword'; import { getAllPuzzles } from './puzzle'; import { + getAllPuzzleInstances, getPuzzleInstance, createPuzzleInstance, submitPuzzleInstance @@ -12,6 +13,7 @@ export { signUp, resetPassword, getAllPuzzles, + getAllPuzzleInstances, getPuzzleInstance, createPuzzleInstance, submitPuzzleInstance, diff --git a/src/styles/abstracts/_variables.scss b/src/styles/abstracts/_variables.scss index 6107abf..5d851ef 100644 --- a/src/styles/abstracts/_variables.scss +++ b/src/styles/abstracts/_variables.scss @@ -25,6 +25,7 @@ $info: $white; $light: $white; $shadow-color: rgba($black, 0.15); +$grey-border: #9a9a9a; // Product Pages $max-product-content-width: 1200px; diff --git a/src/styles/pages/admin.module.scss b/src/styles/pages/admin.module.scss index 7805267..d4ad717 100644 --- a/src/styles/pages/admin.module.scss +++ b/src/styles/pages/admin.module.scss @@ -1,5 +1,34 @@ +@import '../abstracts/variables'; + +.adminTitle { + padding: 0.5rem 3rem; + margin: 0; + background-color: $white; +} + +.adminHeaderSep { + margin: 0; + margin-bottom: 1rem; +} + .contentWrap { - display: flex; - padding: 0 3rem; - gap: 5rem; + display: grid; + grid-template-columns: 5fr 3fr; + + .contentLeftWrap { + display: flex; + flex-direction: column; + padding: 0 3rem; + gap: 3rem; + margin-right: 5rem; + } +} + +.adminSeperator { + height: 100%; + border-left: 1px solid $grey-border; + position: absolute; + top: 0; + left: calc(100% / (5 + 3) * 5 - 2%); + z-index: -1; } diff --git a/src/types/global.ts b/src/types/global.ts index 10ea25d..d3b765f 100644 --- a/src/types/global.ts +++ b/src/types/global.ts @@ -20,5 +20,8 @@ export type InputProps = { maxLength?: number; minLength?: number; labelText?: string; + value?: string; setInputVal?: Dispatch>; + disabled?: boolean; + onClick?: () => void; }; diff --git a/src/types/map.ts b/src/types/map.ts index 1f3e751..9136f7f 100644 --- a/src/types/map.ts +++ b/src/types/map.ts @@ -12,4 +12,6 @@ export type MapRendererProps = { userMarker: MapMarker; mapCenter: MapAnchor; setMapCenter: Dispatch>; + tempMarker?: MapMarker; + setTempMarker?: Dispatch>; }; diff --git a/src/utils/getLinkToPuzzleInstance.ts b/src/utils/getLinkToPuzzleInstance.ts new file mode 100644 index 0000000..07fa0ba --- /dev/null +++ b/src/utils/getLinkToPuzzleInstance.ts @@ -0,0 +1,4 @@ +const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; + +export const getLinkToPuzzleInstance = (puzzleInstanceId: string | number) => + `${BASE_URL}/puzzles/${puzzleInstanceId}`;