diff --git a/.gitattributes b/.gitattributes index a159a710..e2d56c68 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ .snyk linguist-language=YAML .firebaserc linguist-language=JSON +firebase.storage.rules linguist-language=Cloud-Firestore-Security-Rules diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index dcb8b624..71c33206 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -56,6 +56,7 @@ jobs: with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_YASH_TOTALE }}" + target: public projectId: yash-totale env: FIREBASE_CLI_PREVIEWS: hostingchannels diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 883c9674..484703c2 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["hack4impact.h4i-recommendations"] + "recommendations": ["hack4impact.h4i-recommendations", "toba.vsfire"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index adab98a2..e610acb2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ ".snyk": "yaml", ".imgbotconfig": "json", - ".firebaserc": "json" + ".firebaserc": "json", + "firebase.storage.rules": "firerules" } } diff --git a/firebase.json b/firebase.json index 893d8cb0..7d5a445b 100644 --- a/firebase.json +++ b/firebase.json @@ -3,6 +3,9 @@ "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, + "storage": { + "rules": "firebase.storage.rules" + }, "hosting": [ { "target": "public", diff --git a/firebase.storage.rules b/firebase.storage.rules new file mode 100644 index 00000000..776621d8 --- /dev/null +++ b/firebase.storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if request.auth != null; + } + } +} diff --git a/package-lock.json b/package-lock.json index 10f1ff8e..123aa976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,11 +24,13 @@ "firebase": "^8.3.3", "lodash.startcase": "^4.4.0", "lodash.throttle": "^4.1.1", + "mime": "^2.5.2", "moment": "^2.29.1", "notistack": "^1.0.5", "preval.macro": "^5.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-firebaseui": "^5.0.2", "react-google-recaptcha": "^2.1.0", "react-helmet": "^6.1.0", "react-hook-form": "^7.7.1", @@ -50,6 +52,7 @@ "@types/lodash.merge": "^4.6.6", "@types/lodash.startcase": "^4.4.6", "@types/lodash.throttle": "^4.1.6", + "@types/mime": "^2.0.3", "@types/node": "^14.14.34", "@types/prettier": "^2.2.3", "@types/preval.macro": "^3.0.0", @@ -3352,6 +3355,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, + "node_modules/@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -7604,6 +7613,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/dialog-polyfill": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/dialog-polyfill/-/dialog-polyfill-0.4.10.tgz", + "integrity": "sha512-j5yGMkP8T00UFgyO+78OxiN5vC5dzRQF3BEio+LhNvDbyfxWBsi3sfPArDm54VloaJwy2hm3erEiDWqHRC8rzw==" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -9678,6 +9692,18 @@ "node": "^8.13.0 || >=10.10.0" } }, + "node_modules/firebaseui": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/firebaseui/-/firebaseui-4.8.1.tgz", + "integrity": "sha512-Qh8kfqGjMIiVJ2X8MUFsmlf43QFcVc8ungD+kw5T8ACuhQ68IAyUHExlItAfumrcLlqEgyo1MjH0O9fZZAMOKw==", + "dependencies": { + "dialog-polyfill": "^0.4.7", + "material-design-lite": "^1.2.0" + }, + "peerDependencies": { + "firebase": ">=8.2.4" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -14290,6 +14316,14 @@ "integrity": "sha512-vRTPqSU4JK8vVXmjICHSBhwXUvbfh/VJo+j7hvxqe15tLJyomv3FLgFdFgb8kpj0Fe8SsJa/TZUAXv7/sN+N7A==", "dev": true }, + "node_modules/material-design-lite": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/material-design-lite/-/material-design-lite-1.3.0.tgz", + "integrity": "sha1-0ATOP+6Zoe63Sni4oyUTSl8RcdM=", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -14425,15 +14459,14 @@ "dev": true }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" + "node": ">=4.0.0" } }, "node_modules/mime-db": { @@ -17697,18 +17730,6 @@ "ms": "^2.1.1" } }, - "node_modules/puppeteer/node_modules/mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/puppeteer/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -18157,6 +18178,18 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-firebaseui": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-firebaseui/-/react-firebaseui-5.0.2.tgz", + "integrity": "sha512-ZfIWfRKlKAkMwEI+41rMPkwdNEQEfBLXV6TVaN5er3cdwMD0vQecWt6YOE4/v1ELziaj10NNRB8yB42V56QBMQ==", + "dependencies": { + "firebaseui": "^4.8.0" + }, + "peerDependencies": { + "firebase": "^8.2.4", + "react": ">=15 <=17" + } + }, "node_modules/react-google-recaptcha": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", @@ -20046,6 +20079,18 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -20461,18 +20506,6 @@ "node": ">= 10" } }, - "node_modules/sirv/node_modules/mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -23304,18 +23337,6 @@ "node": ">= 6" } }, - "node_modules/webpack-dev-middleware/node_modules/mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/webpack-dev-server": { "version": "3.11.1", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz", @@ -27929,6 +27950,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -31589,6 +31616,11 @@ } } }, + "dialog-polyfill": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/dialog-polyfill/-/dialog-polyfill-0.4.10.tgz", + "integrity": "sha512-j5yGMkP8T00UFgyO+78OxiN5vC5dzRQF3BEio+LhNvDbyfxWBsi3sfPArDm54VloaJwy2hm3erEiDWqHRC8rzw==" + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -33379,6 +33411,15 @@ "@firebase/util": "0.4.1" } }, + "firebaseui": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/firebaseui/-/firebaseui-4.8.1.tgz", + "integrity": "sha512-Qh8kfqGjMIiVJ2X8MUFsmlf43QFcVc8ungD+kw5T8ACuhQ68IAyUHExlItAfumrcLlqEgyo1MjH0O9fZZAMOKw==", + "requires": { + "dialog-polyfill": "^0.4.7", + "material-design-lite": "^1.2.0" + } + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -37238,6 +37279,11 @@ "integrity": "sha512-vRTPqSU4JK8vVXmjICHSBhwXUvbfh/VJo+j7hvxqe15tLJyomv3FLgFdFgb8kpj0Fe8SsJa/TZUAXv7/sN+N7A==", "dev": true }, + "material-design-lite": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/material-design-lite/-/material-design-lite-1.3.0.tgz", + "integrity": "sha1-0ATOP+6Zoe63Sni4oyUTSl8RcdM=" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -37362,10 +37408,9 @@ } }, "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" }, "mime-db": { "version": "1.46.0", @@ -40122,12 +40167,6 @@ } } }, - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -40495,6 +40534,14 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-firebaseui": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-firebaseui/-/react-firebaseui-5.0.2.tgz", + "integrity": "sha512-ZfIWfRKlKAkMwEI+41rMPkwdNEQEfBLXV6TVaN5er3cdwMD0vQecWt6YOE4/v1ELziaj10NNRB8yB42V56QBMQ==", + "requires": { + "firebaseui": "^4.8.0" + } + }, "react-google-recaptcha": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", @@ -42100,6 +42147,12 @@ } } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -42468,14 +42521,6 @@ "@polka/url": "^1.0.0-next.15", "mime": "^2.3.1", "totalist": "^1.0.0" - }, - "dependencies": { - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true - } } }, "sisteransi": { @@ -45097,14 +45142,6 @@ "mkdirp": "^0.5.1", "range-parser": "^1.2.1", "webpack-log": "^2.0.0" - }, - "dependencies": { - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true - } } }, "webpack-dev-server": { diff --git a/package.json b/package.json index 067911c1..0f0d5a41 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "deploy:local": "serve -s build", "deploy:private": "firebase deploy --only hosting:private", "deploy:public": "firebase deploy --only hosting:public", + "deploy:firestore:rules": "firebase deploy --only firestore:rules", + "deploy:storage:rules": "firebase deploy --only storage:rules", "data": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node scripts/data/index.ts", "books": "npm run books:get && npm run books:upload", "books:get": "sh scripts/books/get-books.sh", @@ -51,11 +53,13 @@ "firebase": "^8.3.3", "lodash.startcase": "^4.4.0", "lodash.throttle": "^4.1.1", + "mime": "^2.5.2", "moment": "^2.29.1", "notistack": "^1.0.5", "preval.macro": "^5.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-firebaseui": "^5.0.2", "react-google-recaptcha": "^2.1.0", "react-helmet": "^6.1.0", "react-hook-form": "^7.7.1", @@ -77,6 +81,7 @@ "@types/lodash.merge": "^4.6.6", "@types/lodash.startcase": "^4.4.6", "@types/lodash.throttle": "^4.1.6", + "@types/mime": "^2.0.3", "@types/node": "^14.14.34", "@types/prettier": "^2.2.3", "@types/preval.macro": "^3.0.0", diff --git a/src/App.tsx b/src/App.tsx index a109a3f1..55320acd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { SIDEBAR_WIDTH } from "./Utils/constants"; // Context import { HeadProvider } from "./Context/HeadContext"; import { ClassnameProvider } from "./Context/ClassnameContext"; +import { UserProvider } from "./Context/UserContext"; // Components import Navbar from "./Components/Static/Navbar"; @@ -34,13 +35,15 @@ const CertificationPage = lazy(() => import("./Pages/Certification")); const BooksPage = lazy(() => import("./Pages/Books")); const Contact = lazy(() => import("./Pages/Contact")); const Colors = lazy(() => import("./Pages/Colors")); +const Settings = lazy(() => import("./Pages/Settings")); const NotFound = lazy(() => import("./Pages/NotFound")); const useStyles = makeStyles((theme) => ({ root: { paddingTop: theme.spacing(2), [theme.breakpoints.up("lg")]: { - marginLeft: SIDEBAR_WIDTH, + marginLeft: theme.direction === "ltr" ? SIDEBAR_WIDTH : 0, + marginRight: theme.direction === "rtl" ? SIDEBAR_WIDTH : 0, }, }, layout: { @@ -57,10 +60,12 @@ const App: FC = () => { return ( - - - - + + + + + + ); @@ -116,6 +121,9 @@ const Routes: FC = () => { + + + diff --git a/src/Components/Static/Footer/Footer.tsx b/src/Components/Static/Footer/Footer.tsx index c9834b94..9e69a08e 100644 --- a/src/Components/Static/Footer/Footer.tsx +++ b/src/Components/Static/Footer/Footer.tsx @@ -6,7 +6,14 @@ import LinkIcon from "../../Atomic/Icon/Link"; import { BUILD_TIME } from "../../../Utils/constants"; // Material UI Imports -import { Box, Container, makeStyles, Typography } from "@material-ui/core"; +import { + Box, + Container, + makeStyles, + Typography, + useMediaQuery, + useTheme, +} from "@material-ui/core"; import { GitHub, LinkedIn, Mail } from "@material-ui/icons"; const useStyles = makeStyles((theme) => ({ @@ -56,6 +63,8 @@ const socials: Social[] = [ const Footer: FC = () => { const classes = useStyles(); + const theme = useTheme(); + const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); return ( @@ -70,7 +79,7 @@ const Footer: FC = () => { /> ))} - + Copyright © Yash Totale {new Date().getFullYear()} diff --git a/src/Components/Static/Navbar/Navbar.tsx b/src/Components/Static/Navbar/Navbar.tsx index 4d2bef21..9b8f8094 100644 --- a/src/Components/Static/Navbar/Navbar.tsx +++ b/src/Components/Static/Navbar/Navbar.tsx @@ -25,6 +25,7 @@ import { Brightness4, Menu as MenuButton, Palette, + Settings, } from "@material-ui/icons"; const useStyles = makeStyles((theme) => ({ @@ -34,11 +35,13 @@ const useStyles = makeStyles((theme) => ({ toolbar: { margin: 0, [theme.breakpoints.up("lg")]: { - marginLeft: SIDEBAR_WIDTH, + marginLeft: theme.direction === "ltr" ? SIDEBAR_WIDTH : 0, + marginRight: theme.direction === "rtl" ? SIDEBAR_WIDTH : 0, }, }, - rightIcons: { - marginLeft: "auto", + otherIcons: { + marginLeft: theme.direction === "ltr" ? "auto" : 0, + marginRight: theme.direction === "rtl" ? "auto" : 0, }, avatar: { cursor: "pointer", @@ -57,21 +60,24 @@ const Navbar: FC = () => { const isSizeSmall = useMediaQuery(theme.breakpoints.down("md")); const isDarkMode = theme.palette.type === "dark"; + const isLTR = theme.direction === "ltr"; + + const toggleSidebarButton = ( +
+ + dispatch(toggleSidebar())}> + + + +
+ ); return ( <> -
- {isSizeSmall && ( - - dispatch(toggleSidebar())}> - - - - )} -
-
+ {isSizeSmall && isLTR && toggleSidebarButton} +
{ {isDarkMode ? : } + + + + +
+ {isSizeSmall && !isLTR && toggleSidebarButton} diff --git a/src/Components/Static/Settings/Item.tsx b/src/Components/Static/Settings/Item.tsx new file mode 100644 index 00000000..6eb212cd --- /dev/null +++ b/src/Components/Static/Settings/Item.tsx @@ -0,0 +1,137 @@ +// React Imports +import React, { FC } from "react"; + +// Material UI Imports +import { + makeStyles, + MenuItem, + MenuItemProps, + Select, + Switch, + SwitchProps, + TextField, + TextFieldProps, + Typography, + useMediaQuery, + useTheme, +} from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + item: { + display: "flex", + alignItems: "center", + width: "100%", + padding: theme.spacing(0.75, 6.5), + + [theme.breakpoints.only("xs")]: { + padding: theme.spacing(0.5, 3, 0.75, 6), + }, + }, + label: { + flexGrow: 1, + }, +})); + +interface ItemProps { + label: string; + action: JSX.Element; +} + +type ExtendItem = Omit; + +const Item: FC = (props) => { + const classes = useStyles(); + const theme = useTheme(); + const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); + + return ( +
+ + {props.label} + + {props.action} +
+ ); +}; + +type SwitchItemProps = ExtendItem & { + checked: boolean; + onChange: () => void; + color?: SwitchProps["color"]; +}; + +export const SwitchItem: FC = (props) => { + const theme = useTheme(); + const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); + + return ( + + } + /> + ); +}; + +type SelectInputProps = ExtendItem & { + value: T; + values: T[] | readonly T[]; + onChange: (value: T) => void; + defaultValue?: T; +}; + +export const SelectInput = ( + props: SelectInputProps +): JSX.Element => ( + props.onChange(e.target.value as T)} + > + {props.values.map((val: T, i: number) => ( + + {val} + + ))} + + } + /> +); + +type InputItemProps = ExtendItem & { + value: T; + onChange: (value: T) => void; + type?: TextFieldProps["type"]; +}; + +export const InputItem = ( + props: InputItemProps +): JSX.Element => { + return ( + props.onChange(e.target.value as T)} + type={props.type} + /> + } + /> + ); +}; + +export default Item; diff --git a/src/Components/Static/Settings/Section.tsx b/src/Components/Static/Settings/Section.tsx new file mode 100644 index 00000000..0f4c0ec5 --- /dev/null +++ b/src/Components/Static/Settings/Section.tsx @@ -0,0 +1,57 @@ +// React Imports +import React, { FC } from "react"; +import clsx from "clsx"; +import HorizontalDivider from "../../Atomic/Divider/Horizontal"; + +// Material UI Imports +import { makeStyles, Typography } from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + section: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + width: "100%", + borderTop: `2px solid ${theme.palette.text.disabled}`, + margin: theme.spacing(2, 0), + backgroundColor: + theme.palette.type === "dark" + ? theme.palette.grey[800] + : theme.palette.grey[200], + }, + title: { + width: "100%", + fontWeight: theme.typography.fontWeightMedium, + padding: theme.spacing(2, 1), + + [theme.breakpoints.only("xs")]: { + padding: theme.spacing(1), + }, + }, +})); + +interface SectionProps { + title: string; + className?: string; +} + +const Section: FC = (props) => { + const classes = useStyles(); + + return ( +
+ + {props.title} + + + {props.children} +
+ ); +}; + +export default Section; diff --git a/src/Components/Static/Settings/Sections/Display.tsx b/src/Components/Static/Settings/Sections/Display.tsx new file mode 100644 index 00000000..26ae2789 --- /dev/null +++ b/src/Components/Static/Settings/Sections/Display.tsx @@ -0,0 +1,90 @@ +// React Imports +import React, { FC } from "react"; +import { useLocation } from "react-router-dom"; +import { useTitle } from "../../../../Context/HeadContext"; +import { generateSearch } from "../../../../Utils/funcs"; +import Section from "../Section"; +import Subsection from "../Subsection"; +import Item, { SelectInput, SwitchItem } from "../Item"; +import StyledLink from "../../../Atomic/StyledLink"; + +// Redux Imports +import { useSelector } from "react-redux"; +import { + toggleDarkMode, + getSpacing, + changeSpacing, + getDirection, + changeDirection, +} from "../../../../Redux"; +import { + DEFAULT_DIRECTION, + DEFAULT_SPACING, + DIRECTIONS, + SPACINGS, +} from "../../../../Redux/display.slice"; +import { useAppDispatch } from "../../../../Store"; + +// Material UI Imports +import { useTheme } from "@material-ui/core"; +import { Computer, SettingsBrightness } from "@material-ui/icons"; + +const Display: FC = () => { + const dispatch = useAppDispatch(); + const location = useLocation(); + const title = useTitle(); + const theme = useTheme(); + const isDarkMode = theme.palette.type === "dark"; + + const spacing = useSelector(getSpacing); + const direction = useSelector(getDirection); + + return ( +
+ }> + dispatch(toggleDarkMode())} + /> + + Colors Page + + } + /> + + }> + dispatch(changeSpacing(val))} + /> + dispatch(changeDirection(val))} + /> + +
+ ); +}; + +export default Display; diff --git a/src/Components/Static/Settings/Sections/Profile/LoggedIn.tsx b/src/Components/Static/Settings/Sections/Profile/LoggedIn.tsx new file mode 100644 index 00000000..b293703d --- /dev/null +++ b/src/Components/Static/Settings/Sections/Profile/LoggedIn.tsx @@ -0,0 +1,290 @@ +// React Imports +import React, { FC, useState } from "react"; +import { getExtension } from "mime"; +import { useClosableSnackbar } from "../../../../../Hooks"; +import { User } from "../../../../../Context/UserContext"; + +// Firebase Imports +import "firebase/auth"; +import "firebase/storage"; +import { useAuth, useStorage } from "../../../../../Utils/Config/firebase"; + +// Material UI Imports +import { + Avatar, + Button, + CircularProgress, + InputAdornment, + makeStyles, + TextField, + Tooltip, + useMediaQuery, + useTheme, +} from "@material-ui/core"; +import { Check, CloudUpload } from "@material-ui/icons"; +import clsx from "clsx"; + +const useStyles = makeStyles((theme) => ({ + container: { + display: "flex", + flexDirection: "column", + alignItems: "center", + width: "100%", + padding: theme.spacing(2), + }, + info: { + display: "flex", + justifyContent: "center", + alignItems: "center", + margin: theme.spacing(0, 3, 2), + + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + }, + }, + input: { + margin: theme.spacing(1, 1.5), + + [theme.breakpoints.down("sm")]: { + width: "100%", + margin: theme.spacing(1, 0), + }, + }, + saveIcon: { + cursor: "pointer", + }, + savingSpinner: { + color: theme.palette.text.primary, + }, + emailInput: { + cursor: "not-allowed", + }, + logout: { + borderColor: theme.palette.error.main, + color: theme.palette.error.main, + marginLeft: "auto", + + [theme.breakpoints.down("sm")]: { + marginLeft: 0, + }, + }, +})); + +interface LoggedInProps { + user: User; +} + +const LoggedIn: FC = (props) => { + const classes = useStyles(); + const theme = useTheme(); + const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); + + return ( +
+
+ + + +
+ +
+ ); +}; + +const useProfilePictureStyles = makeStyles((theme) => ({ + container: { + display: "flex", + justifyContent: "center", + alignItems: "center", + height: 56, + width: 56, + margin: theme.spacing(1, 1.5), + cursor: "pointer", + position: "relative", + }, + fileInput: { + position: "absolute", + left: "0px", + top: "0px", + opacity: 0, + width: "100%", + height: "100%", + }, + avatar: { + width: "100%", + height: "100%", + }, + avatarDimmed: { + opacity: 0.5, + }, + spinner: { + position: "absolute", + color: theme.palette.text.primary, + zIndex: 1000, + }, +})); + +const ProfilePicture: FC = (props) => { + const classes = useProfilePictureStyles(); + const theme = useTheme(); + const storage = useStorage(); + const { enqueueSnackbar } = useClosableSnackbar(); + const [uploading, setUploading] = useState(false); + + const onUpload = async (files: FileList | null) => { + if (files === null) return; + + const file = files[0]; + if (!file) return; + + try { + setUploading(true); + const size = file.size / 1000 / 1000; // Size in MB + if (size > 5) { + const formattedSize = size.toFixed(1); + throw new Error( + `File size must be less than 5 MB (Uploaded file size was ${formattedSize} MB)` + ); + } + const ext = getExtension(file.type); + const ref = storage + .ref() + .child(`users/${props.user.id}/profile_picture${ext ? `.${ext}` : ""}`); + const upload = await ref.put(file); + const url = await upload.ref.getDownloadURL(); + await props.user.updatePicture(url); + enqueueSnackbar("Uploaded New Profile Picture", { + variant: "success", + }); + } catch (e: any) { + const message = typeof e === "string" ? e : e.message; + enqueueSnackbar(message || "An error occurred. Please try again.", { + variant: "error", + }); + } finally { + setUploading(false); + } + }; + + return ( + + ); +}; + +const NameField: FC = (props) => { + const classes = useStyles(); + const theme = useTheme(); + const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); + const { enqueueSnackbar } = useClosableSnackbar(); + const [name, setName] = useState(props.user.name); + const [isNameSaving, setNameSaving] = useState(false); + + const onNameSave = async () => { + setNameSaving(true); + try { + await props.user.updateName(name); + enqueueSnackbar("Saved Name", { + variant: "success", + }); + } catch (e: any) { + const message = typeof e === "string" ? e : e.message; + enqueueSnackbar(message || "An error occurred. Please try again.", { + variant: "error", + }); + } finally { + setNameSaving(false); + } + }; + + return ( + setName(e.target.value)} + name="name" + type="text" + label="Name" + variant="outlined" + size={isSizeXS ? "small" : "medium"} + InputProps={{ + endAdornment: ( + + {name === props.user.name ? ( + + + + ) : isNameSaving ? ( + + ) : ( + + + + )} + + ), + }} + className={classes.input} + /> + ); +}; + +const SignOutButton: FC = () => { + const classes = useStyles(); + const auth = useAuth(); + const { enqueueSnackbar } = useClosableSnackbar(); + + return ( + + ); +}; + +export default LoggedIn; diff --git a/src/Components/Static/Settings/Sections/Profile/LoggedOut.tsx b/src/Components/Static/Settings/Sections/Profile/LoggedOut.tsx new file mode 100644 index 00000000..e646d687 --- /dev/null +++ b/src/Components/Static/Settings/Sections/Profile/LoggedOut.tsx @@ -0,0 +1,60 @@ +// React Imports +import React, { FC } from "react"; +import { useClosableSnackbar } from "../../../../../Hooks"; + +// Firebase Imports +import "firebase/auth"; +import firebase, { useAuth } from "../../../../../Utils/Config/firebase"; +import { StyledFirebaseAuth } from "react-firebaseui"; + +// Material UI Imports +import { makeStyles, Typography } from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + container: { + margin: theme.spacing(2), + }, +})); + +const NotLoggedIn: FC = () => { + const classes = useStyles(); + const auth = useAuth(); + const { enqueueSnackbar } = useClosableSnackbar(); + + return ( +
+ You are not signed in. + +
+ ); +}; + +export default NotLoggedIn; diff --git a/src/Components/Static/Settings/Sections/Profile/index.tsx b/src/Components/Static/Settings/Sections/Profile/index.tsx new file mode 100644 index 00000000..8c6ff62a --- /dev/null +++ b/src/Components/Static/Settings/Sections/Profile/index.tsx @@ -0,0 +1,20 @@ +// React Imports +import React, { FC } from "react"; +import LoggedIn from "./LoggedIn"; +import NotLoggedIn from "./LoggedOut"; +import Section from "../../Section"; +import { useUser } from "../../../../../Context/UserContext"; +import HorizontalDivider from "../../../../Atomic/Divider/Horizontal"; + +const Profile: FC = () => { + const user = useUser(); + + return ( +
+ {user ? : } + +
+ ); +}; + +export default Profile; diff --git a/src/Components/Static/Settings/Subsection.tsx b/src/Components/Static/Settings/Subsection.tsx new file mode 100644 index 00000000..2f82b3cf --- /dev/null +++ b/src/Components/Static/Settings/Subsection.tsx @@ -0,0 +1,72 @@ +// React Imports +import React, { cloneElement, FC } from "react"; +import clsx from "clsx"; +import HorizontalDivider from "../../Atomic/Divider/Horizontal"; + +// Material UI Imports +import { + makeStyles, + Typography, + useMediaQuery, + useTheme, +} from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + subsection: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + width: "100%", + }, + titleContainer: { + display: "flex", + justifyContent: "center", + alignItems: "center", + width: "100%", + padding: theme.spacing(2, 1, 0.5), + }, + icon: { + marginLeft: theme.spacing(1), + }, + title: { + flexGrow: 1, + marginLeft: theme.spacing(1.5), + }, +})); + +interface SubsectionProps { + title: string; + icon: JSX.Element; + className?: string; +} + +const Subsection: FC = (props) => { + const classes = useStyles(); + const theme = useTheme(); + const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); + + const icon = cloneElement(props.icon, { + className: classes.icon, + fontSize: isSizeXS ? "small" : "default", + }); + + return ( +
+
+ {icon} + + {props.title} + +
+ {props.children} + +
+ ); +}; + +export default Subsection; diff --git a/src/Context/UserContext.tsx b/src/Context/UserContext.tsx new file mode 100644 index 00000000..0191f490 --- /dev/null +++ b/src/Context/UserContext.tsx @@ -0,0 +1,72 @@ +// React Imports +import React, { + createContext, + FC, + useContext, + useEffect, + useState, +} from "react"; + +// Firebase Imports +import "firebase/auth"; +import firebase, { useAuth } from "../Utils/Config/firebase"; + +interface UserInfo { + id: string; + name: string; + email: string; + picture: string; +} + +export interface User extends UserInfo { + updateName: (newName: string) => Promise; + updatePicture: (newPicture: string) => Promise; +} + +const mapFirebaseUser = (user: firebase.User | null): UserInfo | null => { + if (user === null) return user; + return { + id: user.uid, + name: user.displayName ?? "", + email: user.email ?? "", + picture: user.photoURL ?? "", + }; +}; + +const UserContext = createContext(null); + +export const UserProvider: FC = ({ children }) => { + const auth = useAuth(); + const [user, setUser] = useState( + mapFirebaseUser(auth.currentUser) + ); + + const updateName = async (newName: string) => { + await auth.currentUser!.updateProfile({ + displayName: newName, + }); + setUser({ ...user!, name: newName }); + }; + + const updatePicture = async (newPicture: string) => { + await auth.currentUser!.updateProfile({ + photoURL: newPicture, + }); + setUser({ ...user!, picture: newPicture }); + }; + + useEffect( + () => auth.onAuthStateChanged((user) => setUser(mapFirebaseUser(user))), + [auth] + ); + + return ( + + {children} + + ); +}; + +export const useUser = (): User | null => useContext(UserContext); diff --git a/src/Pages/Contact/index.tsx b/src/Pages/Contact/index.tsx index ae7566ea..ebbfa312 100644 --- a/src/Pages/Contact/index.tsx +++ b/src/Pages/Contact/index.tsx @@ -11,6 +11,7 @@ import { import ReCAPTCHA, { ReCAPTCHAProps } from "react-google-recaptcha"; import emailjs from "emailjs-com"; import { useAnalytics, useClosableSnackbar } from "../../Hooks"; +import { useUser } from "../../Context/UserContext"; import HorizontalDivider from "../../Components/Atomic/Divider/Horizontal"; import { generatePageTitle } from "../../Utils/funcs"; @@ -33,12 +34,12 @@ import { import { Rating } from "@material-ui/lab"; const useStyles = makeStyles((theme) => ({ - title: { - marginTop: theme.spacing(0.5), + divider: { + margin: theme.spacing(1.5, 0, 1), }, container: { width: "100%", - marginTop: theme.spacing(1.5), + marginTop: theme.spacing(2), padding: theme.spacing(2), }, form: { @@ -79,11 +80,17 @@ const Contact: FC = () => { const classes = useStyles(); const { enqueueSnackbar } = useClosableSnackbar(); const firestore = useFirestore(); + const user = useUser(); const theme = useTheme(); const isSizeXS = useMediaQuery(theme.breakpoints.only("xs")); - const { formState, control, handleSubmit, reset } = useForm(); + const { formState, control, handleSubmit, reset } = useForm({ + defaultValues: { + name: user?.name, + email: user?.email, + }, + }); const [loading, setLoading] = useState(false); const recaptchaRef = useRef(null); @@ -119,6 +126,7 @@ const Contact: FC = () => { Object.entries(inputs).filter(([_, v]) => v !== undefined) ); data.timestamp = new Date(); + data.user = user === null ? null : user.id; data["g-recaptcha-response"] = recaptcha; try { @@ -144,7 +152,7 @@ const Contact: FC = () => { bugs: "", }); recaptchaRef.current?.reset(); - } catch (e) { + } catch (e: any) { const message = (typeof e === "string" ? e : e.message) || "An error occurred. Please try again."; @@ -171,8 +179,8 @@ const Contact: FC = () => { {generatePageTitle("Contact")} - - + + Let's get in touch! diff --git a/src/Pages/Settings/index.tsx b/src/Pages/Settings/index.tsx new file mode 100644 index 00000000..acf6b21a --- /dev/null +++ b/src/Pages/Settings/index.tsx @@ -0,0 +1,45 @@ +// React Imports +import React, { FC } from "react"; +import { Helmet } from "react-helmet"; +import { generatePageTitle } from "../../Utils/funcs"; +import HorizontalDivider from "../../Components/Atomic/Divider/Horizontal"; +import Profile from "../../Components/Static/Settings/Sections/Profile"; +import Display from "../../Components/Static/Settings/Sections/Display"; + +// Material UI Imports +import { makeStyles, Typography } from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + divider: { + margin: theme.spacing(1.5, 0, 1), + }, + settings: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "stretch", + width: "100%", + }, +})); + +const Settings: FC = () => { + const classes = useStyles(); + + return ( + <> + + {generatePageTitle("Settings")} + +
+ + + Settings + + + +
+ + ); +}; + +export default Settings; diff --git a/src/Redux/display.slice.ts b/src/Redux/display.slice.ts index e3e5f1ad..febec638 100644 --- a/src/Redux/display.slice.ts +++ b/src/Redux/display.slice.ts @@ -1,6 +1,14 @@ +import { Direction } from "@material-ui/core"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "../Store"; +export const SPACINGS = [6, 8, 10] as const; +export const DEFAULT_SPACING = 8 as const; +export type Spacing = typeof SPACINGS[number]; + +export const DIRECTIONS = ["ltr", "rtl"] as const; +export const DEFAULT_DIRECTION = "ltr" as const; + export const SCHEMES = ["primary", "secondary"] as const; export type Scheme = typeof SCHEMES[number]; @@ -45,6 +53,8 @@ export type Shade = typeof SHADES[number]; export interface DisplayState { isDarkMode: boolean | null; isSidebarOpen: boolean; + spacing: Spacing; + direction: Direction; colors: Record; shades: Record; } @@ -52,6 +62,8 @@ export interface DisplayState { export const initialDisplayState: DisplayState = { isDarkMode: null, isSidebarOpen: false, + spacing: DEFAULT_SPACING, + direction: DEFAULT_DIRECTION, colors: { primary: "lightBlue", secondary: "amber", @@ -74,6 +86,17 @@ const displaySlice = createSlice({ ...state, isSidebarOpen: action.payload ?? !state.isSidebarOpen, }), + changeSpacing: (state, action: PayloadAction) => ({ + ...state, + spacing: action.payload, + }), + changeDirection: ( + state, + action: PayloadAction + ) => ({ + ...state, + direction: action.payload, + }), changeColor: ( state, action: PayloadAction> @@ -123,6 +146,8 @@ const displaySlice = createSlice({ export const { toggleDarkMode, toggleSidebar, + changeSpacing, + changeDirection, changeColor, changeShade, changeShadeAndColors, @@ -137,6 +162,12 @@ export const getIsSidebarOpen = ( state: RootState ): DisplayState["isSidebarOpen"] => state.display.isSidebarOpen; +export const getSpacing = (state: RootState): DisplayState["spacing"] => + state.display.spacing; + +export const getDirection = (state: RootState): DisplayState["direction"] => + state.display.direction; + export const getColors = (state: RootState): DisplayState["colors"] => state.display.colors; diff --git a/src/Redux/index.ts b/src/Redux/index.ts index 55a37d1b..4273d20e 100644 --- a/src/Redux/index.ts +++ b/src/Redux/index.ts @@ -8,12 +8,16 @@ export { // -> Selectors getIsDarkMode, getIsSidebarOpen, + getSpacing, + getDirection, getColors, getShades, getIsDefaultColors, // -> Actions toggleDarkMode, toggleSidebar, + changeSpacing, + changeDirection, changeColor, changeShade, changeShadeAndColors, diff --git a/src/Theme.tsx b/src/Theme.tsx index d3c99ac3..62de6cbf 100644 --- a/src/Theme.tsx +++ b/src/Theme.tsx @@ -3,7 +3,15 @@ import React, { FC } from "react"; //Redux Imports import { useSelector } from "react-redux"; -import { getColors, getIsDarkMode, getShades, toggleDarkMode } from "./Redux"; +import { + getColors, + getDirection, + getIsDarkMode, + getShades, + getSpacing, + toggleDarkMode, +} from "./Redux"; +import { DEFAULT_DIRECTION, DEFAULT_SPACING } from "./Redux/display.slice"; import { useAppDispatch } from "./Store"; //Material UI Imports @@ -14,6 +22,7 @@ import { CssBaseline, } from "@material-ui/core"; import * as muiColors from "@material-ui/core/colors"; +import createSpacing from "@material-ui/core/styles/createSpacing"; export const alternativeFont = "Arial, sans-serif"; @@ -21,6 +30,8 @@ const Theme: FC = ({ children }) => { const dispatch = useAppDispatch(); const colors = useSelector(getColors); const shades = useSelector(getShades); + const spacing = useSelector(getSpacing); + const direction = useSelector(getDirection); const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); const isDarkMode = useSelector(getIsDarkMode); @@ -47,6 +58,8 @@ const Theme: FC = ({ children }) => { }, }, }, + direction: direction ?? DEFAULT_DIRECTION, + spacing: createSpacing(spacing ?? DEFAULT_SPACING), palette: { type: isDarkMode ? "dark" : "light", primary: { diff --git a/src/Utils/Config/firebase.ts b/src/Utils/Config/firebase.ts index 6c0d70f6..2043b52c 100644 --- a/src/Utils/Config/firebase.ts +++ b/src/Utils/Config/firebase.ts @@ -4,7 +4,7 @@ import "firebase/performance"; export const config = { apiKey: "AIzaSyBkV0LzaVCDpgr6-f-60MArbZWlyJ7utYU", - authDomain: "yashtotale.firebaseapp.com", + authDomain: "yash-totale.firebaseapp.com", projectId: "yash-totale", storageBucket: "yash-totale.appspot.com", messagingSenderId: "37331567202", @@ -16,6 +16,14 @@ firebase.initializeApp(config); export const performance = firebase.performance(); +let auth: firebase.auth.Auth; +export const useAuth = (): firebase.auth.Auth => { + if (!auth) { + auth = firebase.auth(); + } + return auth; +}; + let firestore: firebase.firestore.Firestore; export const useFirestore = (): firebase.firestore.Firestore => { if (!firestore) { @@ -24,6 +32,14 @@ export const useFirestore = (): firebase.firestore.Firestore => { return firestore; }; +let storage: firebase.storage.Storage; +export const useStorage = (): firebase.storage.Storage => { + if (!storage) { + storage = firebase.storage(); + } + return storage; +}; + let analytics: firebase.analytics.Analytics; export const useAnalytics = ( triggerCall = true