- complete onyx design template

- implement public sharable urls
- implement more actions
This commit is contained in:
Amruth Pillai
2020-07-11 20:42:16 +05:30
parent 0b5653fab5
commit 5ec1f21bd3
55 changed files with 1025 additions and 412 deletions

View File

@ -1,5 +1,6 @@
{ {
"globals": { "globals": {
"window": true,
"document": true, "document": true,
"FileReader": true, "FileReader": true,
"localStorage": true "localStorage": true

View File

@ -16,3 +16,12 @@ exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
}); });
} }
}; };
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions;
if (page.path.match(/^\/r/)) {
page.matchPath = '/r/*';
createPage(page);
}
};

218
package-lock.json generated
View File

@ -1377,9 +1377,9 @@
} }
}, },
"@grpc/proto-loader": { "@grpc/proto-loader": {
"version": "0.5.4", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz",
"integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==",
"requires": { "requires": {
"lodash.camelcase": "^4.3.0", "lodash.camelcase": "^4.3.0",
"protobufjs": "^6.8.6" "protobufjs": "^6.8.6"
@ -2012,6 +2012,21 @@
"trough": "^1.0.0", "trough": "^1.0.0",
"vfile": "^4.0.0" "vfile": "^4.0.0"
} }
},
"unist-util-is": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz",
"integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ=="
},
"unist-util-visit": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.2.tgz",
"integrity": "sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^4.0.0",
"unist-util-visit-parents": "^3.0.0"
}
} }
} }
}, },
@ -2295,9 +2310,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "13.13.13", "version": "13.13.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.14.tgz",
"integrity": "sha512-UfvBE9oRCAJVzfR+3eWm/sdLFe/qroAPEXP3GPJ1SehQiEVgZT6NQZWYbPMiJ3UdcKM06v4j+S1lTcdWCmw+3g==" "integrity": "sha512-Az3QsOt1U/K1pbCQ0TXGELTuTkPLOiFIQf3ILzbOyo0FqgV9SxRnxbxM5QlAveERZMHpZY+7u3Jz2tKyl+yg6g=="
}, },
"@types/parse-json": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
@ -2324,9 +2339,9 @@
} }
}, },
"@types/react": { "@types/react": {
"version": "16.9.41", "version": "16.9.42",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.41.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.42.tgz",
"integrity": "sha512-6cFei7F7L4wwuM+IND/Q2cV1koQUvJ8iSV+Gwn0c3kvABZ691g7sp3hfEQHOUBJtccl1gPi+EyNjMIl9nGA0ug==", "integrity": "sha512-iGy6HwfVfotqJ+PfRZ4eqPHPP5NdPZgQlr0lTs8EfkODRBV9cYy8QMKcC9qPCe1JrESC1Im6SrCFR6tQgg74ag==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^2.2.0" "csstype": "^2.2.0"
@ -3058,12 +3073,12 @@
"integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==" "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ=="
}, },
"autoprefixer": { "autoprefixer": {
"version": "9.8.4", "version": "9.8.5",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.4.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.5.tgz",
"integrity": "sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==", "integrity": "sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg==",
"requires": { "requires": {
"browserslist": "^4.12.0", "browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001087", "caniuse-lite": "^1.0.30001097",
"colorette": "^1.2.0", "colorette": "^1.2.0",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"num2fraction": "^1.2.2", "num2fraction": "^1.2.2",
@ -3825,6 +3840,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -4371,9 +4394,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001096", "version": "1.0.30001097",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001096.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001097.tgz",
"integrity": "sha512-PFTw9UyVfbkcMEFs82q8XVlRayj7HKvnhu5BLcmjGpv+SNyiWasCcWXPGJuO0rK0dhLRDJmtZcJ+LHUfypbw1w==" "integrity": "sha512-TeuSleKt/vWXaPkLVFqGDnbweYfq4IaZ6rUugFf3rWY6dlII8StUZ8Ddin0PkADfgYZ4wRqCdO2ORl4Rn5eZIA=="
}, },
"caseless": { "caseless": {
"version": "0.12.0", "version": "0.12.0",
@ -5647,12 +5670,9 @@
} }
}, },
"decamelize": { "decamelize": {
"version": "3.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-3.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
"requires": {
"xregexp": "^4.2.4"
}
}, },
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.0", "version": "0.2.0",
@ -6380,9 +6400,9 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.494", "version": "1.3.496",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.494.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.496.tgz",
"integrity": "sha512-EOZuaDT3L1sCIMAVN5J0nGuGWVq5dThrdl0d8XeDYf4MOzbXqZ19OLKesN8TZj0RxtpYjqHpiw/fR6BKWdMwYA==" "integrity": "sha512-TXY4mwoyowwi4Lsrq9vcTUYBThyc1b2hXaTZI13p8/FRhY2CTaq5lK+DVjhYkKiTLsKt569Xes+0J5JsVXFurQ=="
}, },
"elliptic": { "elliptic": {
"version": "6.5.3", "version": "6.5.3",
@ -6421,11 +6441,11 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
}, },
"encoding": { "encoding": {
"version": "0.1.12", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"requires": { "requires": {
"iconv-lite": "~0.4.13" "iconv-lite": "^0.6.2"
} }
}, },
"end-of-stream": { "end-of-stream": {
@ -7594,6 +7614,14 @@
"toidentifier": "1.0.0" "toidentifier": "1.0.0"
} }
}, },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"raw-body": { "raw-body": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
@ -7682,6 +7710,14 @@
"tmp": "^0.0.33" "tmp": "^0.0.33"
}, },
"dependencies": { "dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -8230,9 +8266,9 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
}, },
"gatsby": { "gatsby": {
"version": "2.24.1", "version": "2.24.2",
"resolved": "https://registry.npmjs.org/gatsby/-/gatsby-2.24.1.tgz", "resolved": "https://registry.npmjs.org/gatsby/-/gatsby-2.24.2.tgz",
"integrity": "sha512-aqFfx+Vj3kBhS17tgL1LrkMzip2Xctd3lCj+pogCGV9GCkg6wOqW2uOEqWeoiCNq09sPuwv3GNB8sbEFoQ/2DA==", "integrity": "sha512-2zhCJZBPRJiUGbFRnCogMY3liBoFdb3+cCmIpp5b4BzGUEm+t+QZPSW34xkV5IE1WNywuIMtpZF6G8xTbuepbA==",
"requires": { "requires": {
"@babel/code-frame": "^7.10.3", "@babel/code-frame": "^7.10.3",
"@babel/core": "^7.10.3", "@babel/core": "^7.10.3",
@ -8536,11 +8572,11 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "3.0.4", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz",
"integrity": "sha512-4oT62d2jwSDBbLLFLZE+1vPuQ1h8p9wjrJ8Mqx5TjsyWmBMV5B13eJqn8pvluqubLf3cJPTfiYCIwNwDNmzScQ==", "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==",
"requires": { "requires": {
"lru-cache": "^5.1.1" "lru-cache": "^6.0.0"
} }
}, },
"ignore": { "ignore": {
@ -8549,11 +8585,11 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
}, },
"lru-cache": { "lru-cache": {
"version": "5.1.1", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": { "requires": {
"yallist": "^3.0.2" "yallist": "^4.0.0"
} }
}, },
"node-fetch": { "node-fetch": {
@ -8598,9 +8634,9 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}, },
"yallist": { "yallist": {
"version": "3.1.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
} }
} }
}, },
@ -10223,11 +10259,11 @@
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==" "integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": { "requires": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3.0.0"
} }
}, },
"icss-replace-symbols": { "icss-replace-symbols": {
@ -10866,9 +10902,9 @@
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
}, },
"inquirer": { "inquirer": {
"version": "7.3.0", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.0.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.1.tgz",
"integrity": "sha512-K+LZp6L/6eE5swqIcVXrxl21aGDU4S50gKH0/d96OMQnSBCyGyZl/oZhbkVmdp5sBoINHd4xZvFSARh2dk6DWA==", "integrity": "sha512-/+vOpHQHhoh90Znev8BXiuw1TDQ7IDxWsQnFafUEoK5+4uN5Eoz1p+3GqOj/NtzEi9VzWKQcV9Bm+i8moxedsA==",
"requires": { "requires": {
"ansi-escapes": "^4.2.1", "ansi-escapes": "^4.2.1",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -10876,7 +10912,7 @@
"cli-width": "^3.0.0", "cli-width": "^3.0.0",
"external-editor": "^3.0.3", "external-editor": "^3.0.3",
"figures": "^3.0.0", "figures": "^3.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.16",
"mute-stream": "0.0.8", "mute-stream": "0.0.8",
"run-async": "^2.4.0", "run-async": "^2.4.0",
"rxjs": "^6.6.0", "rxjs": "^6.6.0",
@ -12495,11 +12531,6 @@
"trim-newlines": "^1.0.0" "trim-newlines": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"find-up": { "find-up": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@ -12976,9 +13007,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}, },
"neo-async": { "neo-async": {
"version": "2.6.1", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
}, },
"neon-js": { "neon-js": {
"version": "1.1.2", "version": "1.1.2",
@ -14472,9 +14503,9 @@
} }
}, },
"postcss-nested": { "postcss-nested": {
"version": "4.2.2", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.2.2.tgz", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.2.3.tgz",
"integrity": "sha512-KivGs+ikQlX8VvR9pbaNA/eVmnCN9WcvD8sO9gPqgy6Q6teOH9NqbHHv+czcVJwbBtIdcq/lCzsVgK9daNrhDQ==", "integrity": "sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw==",
"dev": true, "dev": true,
"requires": { "requires": {
"postcss": "^7.0.32", "postcss": "^7.0.32",
@ -15165,6 +15196,14 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
} }
} }
}, },
@ -15325,6 +15364,14 @@
"escape-string-regexp": "^1.0.5" "escape-string-regexp": "^1.0.5"
} }
}, },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inquirer": { "inquirer": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
@ -15691,12 +15738,12 @@
"integrity": "sha512-u5l7fhAJXecWUJzVxzMRU2Zvw8m4QmDNHlTrT5uo3KBlYBhmChd7syAakBoay1yIiVhx/8Fi7a6v6kQZfsw81Q==" "integrity": "sha512-u5l7fhAJXecWUJzVxzMRU2Zvw8m4QmDNHlTrT5uo3KBlYBhmChd7syAakBoay1yIiVhx/8Fi7a6v6kQZfsw81Q=="
}, },
"react-scroll": { "react-scroll": {
"version": "1.7.16", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.7.16.tgz", "resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.8.0.tgz",
"integrity": "sha512-f4M5AdL+3cw3MJ7c/T0hPMY2iHCeQLDXV13lRanAFQ6JIt9xyAdHCpTH9mLUQt9SQh4pRarD+Qc7KhU6qMx3Yg==", "integrity": "sha512-oZfBXPhcxYPR8elI9tC3ORT6+iqiPPJWslsdR9intbNI5PVSa4XoAfC0I/cB3zk5lxQ/NSexCnT+8RqJL8mSZQ==",
"requires": { "requires": {
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"prop-types": "^15.5.8" "prop-types": "^15.7.2"
} }
}, },
"react-side-effect": { "react-side-effect": {
@ -16801,6 +16848,11 @@
"jsonify": "~0.0.0" "jsonify": "~0.0.0"
} }
}, },
"short-unique-id": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-3.0.3.tgz",
"integrity": "sha512-g8StBeiZN4bAtJlZIZQ3C7RNRjtTdJhwgq4WRHC30+z2dbuE/A0Z51CafHsgpwJHYllW4lyH17EKiyBe4W/AeA=="
},
"side-channel": { "side-channel": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz",
@ -18671,9 +18723,9 @@
} }
}, },
"unist-util-visit": { "unist-util-visit": {
"version": "2.0.2", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.2.tgz", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz",
"integrity": "sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ==", "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==",
"requires": { "requires": {
"@types/unist": "^2.0.0", "@types/unist": "^2.0.0",
"unist-util-is": "^4.0.0", "unist-util-is": "^4.0.0",
@ -19449,11 +19501,6 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"del": { "del": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
@ -20132,14 +20179,6 @@
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
"integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
}, },
"xregexp": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
"integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==",
"requires": {
"@babel/runtime-corejs3": "^7.8.3"
}
},
"xss": { "xss": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.7.tgz", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.7.tgz",
@ -20184,12 +20223,12 @@
} }
}, },
"yargs": { "yargs": {
"version": "15.4.0", "version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.0.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw==", "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"requires": { "requires": {
"cliui": "^6.0.0", "cliui": "^6.0.0",
"decamelize": "^3.2.0", "decamelize": "^1.2.0",
"find-up": "^4.1.0", "find-up": "^4.1.0",
"get-caller-file": "^2.0.1", "get-caller-file": "^2.0.1",
"require-directory": "^2.1.1", "require-directory": "^2.1.1",
@ -20268,13 +20307,6 @@
"requires": { "requires": {
"camelcase": "^5.0.0", "camelcase": "^5.0.0",
"decamelize": "^1.2.0" "decamelize": "^1.2.0"
},
"dependencies": {
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
}
} }
}, },
"yauzl": { "yauzl": {

View File

@ -24,7 +24,7 @@
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"firebase": "^7.16.0", "firebase": "^7.16.0",
"formik": "^2.1.4", "formik": "^2.1.4",
"gatsby": "^2.24.1", "gatsby": "^2.24.2",
"gatsby-image": "^2.4.13", "gatsby-image": "^2.4.13",
"gatsby-plugin-create-client-paths": "^2.3.10", "gatsby-plugin-create-client-paths": "^2.3.10",
"gatsby-plugin-firebase": "^0.2.0-beta.4", "gatsby-plugin-firebase": "^0.2.0-beta.4",
@ -48,13 +48,14 @@
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-icons": "^3.10.0", "react-icons": "^3.10.0",
"react-markdown": "^4.3.1", "react-markdown": "^4.3.1",
"react-scroll": "^1.7.16", "react-scroll": "^1.8.0",
"react-toastify": "^6.0.8", "react-toastify": "^6.0.8",
"short-unique-id": "^3.0.3",
"uuid": "^8.2.0", "uuid": "^8.2.0",
"yup": "^0.29.1" "yup": "^0.29.1"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^9.8.4", "autoprefixer": "^9.8.5",
"eslint": "^7.4.0", "eslint": "^7.4.0",
"eslint-config-airbnb": "^18.2.0", "eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",

View File

@ -2,6 +2,7 @@
width: 210mm; width: 210mm;
height: 297mm; height: 297mm;
zoom: 0.8; zoom: 0.8;
overflow: scroll;
box-shadow: var(--shadow); box-shadow: var(--shadow);
@apply bg-white; @apply bg-white rounded;
} }

View File

@ -14,7 +14,7 @@ const LeftNavbar = () => (
<hr className="my-6" /> <hr className="my-6" />
<div className="grid grid-cols-1 gap-4 text-primary-400"> <div className="grid grid-cols-1 gap-4 text-primary-500">
{sections.map((x) => ( {sections.map((x) => (
<SectionIcon <SectionIcon
key={x.id} key={x.id}

View File

@ -1,6 +1,8 @@
import React, { Fragment, memo } from 'react'; import React, { Fragment, memo } from 'react';
import { Element } from 'react-scroll'; import { Element } from 'react-scroll';
import sections from '../../../data/leftSections'; import sections from '../../../data/leftSections';
import LeftNavbar from './LeftNavbar';
import styles from './LeftSidebar.module.css';
import Awards from './sections/Awards'; import Awards from './sections/Awards';
import Certifications from './sections/Certifications'; import Certifications from './sections/Certifications';
import Education from './sections/Education'; import Education from './sections/Education';
@ -8,12 +10,11 @@ import Hobbies from './sections/Hobbies';
import Languages from './sections/Languages'; import Languages from './sections/Languages';
import Objective from './sections/Objective'; import Objective from './sections/Objective';
import Profile from './sections/Profile'; import Profile from './sections/Profile';
import Projects from './sections/Projects';
import References from './sections/References'; import References from './sections/References';
import Skills from './sections/Skills'; import Skills from './sections/Skills';
import Social from './sections/Social'; import Social from './sections/Social';
import Work from './sections/Work'; import Work from './sections/Work';
import LeftNavbar from './LeftNavbar';
import styles from './LeftSidebar.module.css';
const getComponent = (id) => { const getComponent = (id) => {
switch (id) { switch (id) {
@ -27,6 +28,8 @@ const getComponent = (id) => {
return Work; return Work;
case 'education': case 'education':
return Education; return Education;
case 'projects':
return Projects;
case 'awards': case 'awards':
return Awards; return Awards;
case 'certifications': case 'certifications':

View File

@ -0,0 +1,23 @@
import React, { memo } from 'react';
import Heading from '../../../shared/Heading';
import List from '../../lists/List';
const Projects = ({ id, name, event }) => {
const path = `${id}.items`;
return (
<section>
<Heading>{name}</Heading>
<List
path={path}
event={event}
titlePath="title"
subtitlePath="link"
textPath="summary"
/>
</section>
);
};
export default memo(Projects);

View File

@ -42,7 +42,11 @@ const List = ({
subtitle={ subtitle={
subtitle || subtitle ||
get(x, subtitlePath, '') || get(x, subtitlePath, '') ||
(hasDate && formatDateRange(x)) (hasDate &&
formatDateRange({
startDate: x.startDate,
endDate: x.endDate,
}))
} }
text={text || get(x, textPath, '')} text={text || get(x, textPath, '')}
onEdit={() => handleEdit(x)} onEdit={() => handleEdit(x)}

View File

@ -7,7 +7,7 @@ import SyncIndicator from './SyncIndicator';
const RightNavbar = () => { const RightNavbar = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className="grid grid-cols-1 gap-4 text-primary-400"> <div className="grid grid-cols-1 gap-4 text-primary-500">
{sections.map((x) => ( {sections.map((x) => (
<SectionIcon <SectionIcon
key={x.id} key={x.id}

View File

@ -1,18 +1,14 @@
import cx from 'classnames'; import cx from 'classnames';
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import { MdSync, MdSyncDisabled } from 'react-icons/md'; import { MdSync } from 'react-icons/md';
import DatabaseContext from '../../../contexts/DatabaseContext'; import DatabaseContext from '../../../contexts/DatabaseContext';
const SyncIndicator = () => { const SyncIndicator = () => {
const { isOffline, isUpdating } = useContext(DatabaseContext); const { isUpdating } = useContext(DatabaseContext);
return ( return (
<div className="text-4xl"> <div className="text-4xl">
{isOffline ? ( <MdSync className={cx({ spin: isUpdating })} />
<MdSyncDisabled className="text-red-600" />
) : (
<MdSync className={cx({ spin: isUpdating })} />
)}
</div> </div>
); );
}; };

View File

@ -1,36 +1,88 @@
import React, { memo, useContext } from 'react';
import { MdImportExport } from 'react-icons/md';
import { clone } from 'lodash'; import { clone } from 'lodash';
import Heading from '../../../shared/Heading'; import React, { memo, useContext, useState } from 'react';
import Button from '../../../shared/Button'; import { MdImportExport } from 'react-icons/md';
import styles from './Actions.module.css';
import Input from '../../../shared/Input';
import ModalContext from '../../../../contexts/ModalContext'; import ModalContext from '../../../../contexts/ModalContext';
import { useSelector } from '../../../../contexts/ResumeContext'; import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
import UserContext from '../../../../contexts/UserContext';
import Button from '../../../shared/Button';
import Heading from '../../../shared/Heading';
import Input from '../../../shared/Input';
import styles from './Actions.module.css';
const Actions = () => { const Actions = () => {
const [loadDemoText, setLoadDemoText] = useState('Load Demo Data');
const [resetText, setResetText] = useState('Reset Everything');
const [deleteText, setDeleteText] = useState('Delete Account');
const state = useSelector(); const state = useSelector();
const dispatch = useDispatch();
const { emitter, events } = useContext(ModalContext); const { emitter, events } = useContext(ModalContext);
const { deleteAccount } = useContext(UserContext);
const handleImport = () => emitter.emit(events.IMPORT_MODAL); const handleImport = () => emitter.emit(events.IMPORT_MODAL);
const handleExportToJson = () => { const handleExportToJson = () => {
const backupObj = clone(state); const backupObj = clone(state);
delete backupObj.id; delete backupObj.id;
delete backupObj.user;
delete backupObj.name;
delete backupObj.createdAt;
delete backupObj.updatedAt;
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent( const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(backupObj), JSON.stringify(backupObj),
)}`; )}`;
const dlAnchor = document.getElementById('downloadAnchor'); const dlAnchor = document.getElementById('downloadAnchor');
dlAnchor.setAttribute('href', dataStr); dlAnchor.setAttribute('href', dataStr);
dlAnchor.setAttribute('download', `RxResume_${state.id}.json`); dlAnchor.setAttribute(
'download',
`RxResume_${state.id}_${Date.now()}.json`,
);
dlAnchor.click(); dlAnchor.click();
}; };
const getSharableUrl = () => { const getSharableUrl = () => {
const shareId = state.id.split('-')[0]; const shareId = state.id;
return `https://rxresu.me/r/${shareId}`; return `https://rxresu.me/r/${shareId}`;
}; };
const handleOpenLink = () => {
if (typeof window !== `undefined`) {
window && window.open(getSharableUrl());
}
};
const handleLoadDemo = () => {
if (loadDemoText === 'Load Demo Data') {
setLoadDemoText('Are you sure?');
return;
}
dispatch({ type: 'load_demo_data' });
setLoadDemoText('Load Demo Data');
};
const handleReset = () => {
if (resetText === 'Reset Everything') {
setResetText('Are you sure?');
return;
}
setResetText('Reset Everything');
dispatch({ type: 'reset_data' });
};
const handleDeleteAccount = () => {
if (deleteText === 'Delete Account') {
setDeleteText('Are you sure?');
return;
}
setDeleteText('Buh bye! :(');
setTimeout(() => {
deleteAccount();
}, 500);
};
return ( return (
<section> <section>
<Heading>Actions</Heading> <Heading>Actions</Heading>
@ -79,7 +131,11 @@ const Actions = () => {
</p> </p>
<div> <div>
<Input type="action" value={getSharableUrl()} onClick={() => {}} /> <Input
type="action"
value={getSharableUrl()}
onClick={handleOpenLink}
/>
</div> </div>
</div> </div>
@ -92,12 +148,25 @@ const Actions = () => {
</p> </p>
<div className="mt-4 flex"> <div className="mt-4 flex">
<Button>Load Demo Data</Button> <Button onClick={handleLoadDemo}>{loadDemoText}</Button>
</div> </div>
</div> </div>
<div className={styles.container}> <div className={styles.container}>
<h5>Delete Account</h5> <h5>Reset Everything</h5>
<p className="leading-loose">
Feels like you made too many mistakes? No worries, clear everything
with just one click, but be careful if there are no backups.
</p>
<div className="mt-4 flex">
<Button onClick={handleReset}>{resetText}</Button>
</div>
</div>
<div className={styles.container}>
<h5>Danger Zone</h5>
<p className="leading-loose"> <p className="leading-loose">
If you would like to delete your account and erase all your resumes, If you would like to delete your account and erase all your resumes,
@ -106,7 +175,9 @@ const Actions = () => {
</p> </p>
<div className="mt-4 flex"> <div className="mt-4 flex">
<Button isDelete>Delete Account</Button> <Button isDelete onClick={handleDeleteAccount}>
{deleteText}
</Button>
</div> </div>
</div> </div>
</section> </section>

View File

@ -48,7 +48,7 @@ const Layout = () => {
<section> <section>
<Heading>Layout</Heading> <Heading>Layout</Heading>
<p> <p className="leading-loose">
This template supports {blocks.length} blocks. You can re-order or move This template supports {blocks.length} blocks. You can re-order or move
sections by dragging/dropping the section names across lists. sections by dragging/dropping the section names across lists.
</p> </p>

View File

@ -32,7 +32,9 @@ const Hero = () => {
Go to App Go to App
</Button> </Button>
) : ( ) : (
<Button title="Login" onClick={handleLogin} isLoading={loading} /> <Button onClick={handleLogin} isLoading={loading}>
Login
</Button>
)} )}
<Button <Button
outline outline

View File

@ -3,7 +3,7 @@ import React, { memo, useContext } from 'react';
import UserContext from '../../contexts/UserContext'; import UserContext from '../../contexts/UserContext';
import LoadingScreen from './LoadingScreen'; import LoadingScreen from './LoadingScreen';
const PrivateRoute = ({ component: Component, location, ...props }) => { const PrivateRoute = ({ component: Component, ...props }) => {
const { user, loading } = useContext(UserContext); const { user, loading } = useContext(UserContext);
if (loading) { if (loading) {

View File

@ -1,11 +1,7 @@
.container {
@apply w-full;
}
.container label input, .container label input,
.container label textarea, .container label textarea,
.container label select { .container label select {
@apply py-3 px-4 rounded text-primary-900 bg-primary-200 border border-transparent appearance-none; @apply w-full py-3 px-4 rounded text-primary-900 bg-primary-200 border border-transparent appearance-none;
} }
.container label input::placeholder, .container label input::placeholder,

View File

@ -1,50 +0,0 @@
import { Field } from 'formik';
import React, { memo } from 'react';
import { MdAdd } from 'react-icons/md';
import { getFieldProps, handleKeyUp } from '../../utils';
import Input from './Input';
const InputArray = ({ formik, schema, helpers, label, path, placeholder }) => {
const handleClickAdd = () => {
formik.values.temp && helpers.push(formik.values.temp);
formik.setFieldValue('temp', '');
};
return (
<div className="col-span-2">
<label>
<span>{label}</span>
{formik.values[path] &&
formik.values[path].map((x, i) => (
<Field key={i} name={`${path}.${i}`}>
{({ field, meta }) => (
<Input
className="my-1"
onClick={() => helpers.remove(i)}
{...field}
{...meta}
/>
)}
</Field>
))}
<div className="flex items-center">
<Input
placeholder={placeholder}
{...getFieldProps(formik, schema, 'temp')}
/>
<MdAdd
size="18px"
tabIndex="0"
className="mx-4 cursor-pointer opacity-50 hover:opacity-75"
onKeyUp={(e) => handleKeyUp(e, handleClickAdd)}
onClick={handleClickAdd}
/>
</div>
</label>
</div>
);
};
export default memo(InputArray);

View File

@ -1,3 +1,4 @@
import { Tooltip } from '@material-ui/core';
import React, { memo, useContext, useRef } from 'react'; import React, { memo, useContext, useRef } from 'react';
import { MdFileUpload } from 'react-icons/md'; import { MdFileUpload } from 'react-icons/md';
import StorageContext from '../../contexts/StorageContext'; import StorageContext from '../../contexts/StorageContext';
@ -20,27 +21,29 @@ const PhotoUpload = () => {
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<div <Tooltip title="Upload Photograph" placement="right-start">
role="button" <div
tabIndex="0" role="button"
className={styles.circle} tabIndex="0"
onClick={handleIconClick} className={styles.circle}
onKeyUp={(e) => handleKeyUp(e, handleIconClick)} onClick={handleIconClick}
> onKeyUp={(e) => handleKeyUp(e, handleIconClick)}
<MdFileUpload size="22px" /> >
<input <MdFileUpload size="22px" />
name="file" <input
type="file" name="file"
ref={fileInputRef} type="file"
className="hidden" ref={fileInputRef}
onChange={handleImageUpload} className="hidden"
/> onChange={handleImageUpload}
</div> />
</div>
</Tooltip>
<Input <Input
name="photograph" name="photograph"
label="Photograph" label="Photograph"
className="pl-6" className="pl-6 w-full"
path="profile.photograph" path="profile.photograph"
/> />
</div> </div>

View File

@ -1,6 +1,7 @@
import { Tooltip } from '@material-ui/core'; import { Tooltip } from '@material-ui/core';
import React, { memo, useEffect } from 'react'; import React, { memo, useEffect } from 'react';
import { Link, scrollSpy } from 'react-scroll'; import { Link, scrollSpy } from 'react-scroll';
import styles from './SectionIcon.module.css';
const SectionIcon = ({ section, containerId, tooltipPlacement }) => { const SectionIcon = ({ section, containerId, tooltipPlacement }) => {
const { id, name, icon: Icon } = section; const { id, name, icon: Icon } = section;
@ -19,7 +20,7 @@ const SectionIcon = ({ section, containerId, tooltipPlacement }) => {
duration={500} duration={500}
containerId={containerId} containerId={containerId}
activeClass="text-primary-900" activeClass="text-primary-900"
className="py-2 cursor-pointer focus:outline-none focus:text-primary-900 hover:text-primary-900" className={styles.icon}
> >
<Icon size="18px" /> <Icon size="18px" />
</Link> </Link>

View File

@ -0,0 +1,11 @@
.icon {
@apply py-2 cursor-pointer;
}
.icon:focus {
@apply outline-none text-primary-900;
}
.icon:hover {
@apply text-primary-900;
}

View File

@ -4,6 +4,7 @@ const ModalEvents = {
SOCIAL_MODAL: 'social_modal', SOCIAL_MODAL: 'social_modal',
WORK_MODAL: 'work_modal', WORK_MODAL: 'work_modal',
EDUCATION_MODAL: 'education_modal', EDUCATION_MODAL: 'education_modal',
PROJECT_MODAL: 'project_modal',
AWARD_MODAL: 'award_modal', AWARD_MODAL: 'award_modal',
CERTIFICATION_MODAL: 'certification_modal', CERTIFICATION_MODAL: 'certification_modal',
SKILL_MODAL: 'skill_modal', SKILL_MODAL: 'skill_modal',

View File

@ -1,19 +1,13 @@
import firebase from 'gatsby-plugin-firebase'; import firebase from 'gatsby-plugin-firebase';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { import ShortUniqueId from 'short-unique-id';
createContext, import React, { createContext, memo, useContext, useState } from 'react';
memo,
useContext,
useEffect,
useState,
} from 'react';
import UserContext from './UserContext';
import initialState from '../data/initialState'; import initialState from '../data/initialState';
import UserContext from './UserContext';
const DEBOUNCE_WAIT_TIME = 4000; const DEBOUNCE_WAIT_TIME = 4000;
const defaultState = { const defaultState = {
isOffline: false,
isUpdating: false, isUpdating: false,
createResume: () => {}, createResume: () => {},
deleteResume: () => {}, deleteResume: () => {},
@ -26,28 +20,26 @@ const defaultState = {
const DatabaseContext = createContext(defaultState); const DatabaseContext = createContext(defaultState);
const DatabaseProvider = ({ children }) => { const DatabaseProvider = ({ children }) => {
const [resumeId, setResumeId] = useState(false); const dictionary = 'abcdefghijklmnopqrstuvwxyz1234567890'.split('');
const [isOffline, setOffline] = useState(false); const uuid = new ShortUniqueId({ dictionary });
const [isUpdating, setUpdating] = useState(false); const [isUpdating, setUpdating] = useState(false);
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
useEffect(() => {
const connectedRef = firebase.database().ref('.info/connected');
connectedRef.on('value', (snapshot) => {
snapshot.val() === true ? setOffline(false) : setOffline(true);
});
}, []);
const getResume = async (id) => { const getResume = async (id) => {
setResumeId(id); try {
const snapshot = await firebase const snapshot = await firebase
.database() .database()
.ref(`users/${user.uid}/resumes/${id}`) .ref(`resumes/${id}`)
.once('value'); .once('value');
return snapshot.val(); return snapshot.val();
} catch (error) {
return null;
}
}; };
const createResume = ({ id, name }) => { const createResume = ({ name }) => {
const id = uuid();
const createdAt = firebase.database.ServerValue.TIMESTAMP; const createdAt = firebase.database.ServerValue.TIMESTAMP;
let firstName; let firstName;
@ -57,21 +49,21 @@ const DatabaseProvider = ({ children }) => {
[firstName, lastName] = user.displayName.split(' '); [firstName, lastName] = user.displayName.split(' ');
} }
firebase const resume = {
.database() ...initialState,
.ref(`users/${user.uid}/resumes/${id}`) id,
.set({ name,
...initialState, user: user.uid,
id, profile: {
name, ...initialState.profile,
profile: { firstName: firstName || '',
...initialState.profile, lastName: lastName || '',
firstName: firstName || '', },
lastName: lastName || '', createdAt,
}, updatedAt: createdAt,
createdAt, };
updatedAt: createdAt,
}); firebase.database().ref(`resumes/${id}`).set(resume);
}; };
const updateResume = async (resume) => { const updateResume = async (resume) => {
@ -79,7 +71,7 @@ const DatabaseProvider = ({ children }) => {
await firebase await firebase
.database() .database()
.ref(`users/${user.uid}/resumes/${resumeId}`) .ref(`resumes/${resume.id}`)
.update({ .update({
...resume, ...resume,
updatedAt: firebase.database.ServerValue.TIMESTAMP, updatedAt: firebase.database.ServerValue.TIMESTAMP,
@ -90,14 +82,18 @@ const DatabaseProvider = ({ children }) => {
const debouncedUpdateResume = debounce(updateResume, DEBOUNCE_WAIT_TIME); const debouncedUpdateResume = debounce(updateResume, DEBOUNCE_WAIT_TIME);
const deleteResume = (id) => { const deleteResume = async (id) => {
firebase.database().ref(`users/${user.uid}/resumes/${id}`).remove(); await firebase
.storage()
.ref(`/users/${user.uid}/photographs/${id}`)
.delete();
await firebase.database().ref(`/resumes/${id}`).remove();
}; };
return ( return (
<DatabaseContext.Provider <DatabaseContext.Provider
value={{ value={{
isOffline,
isUpdating, isUpdating,
getResume, getResume,
createResume, createResume,

View File

@ -8,6 +8,7 @@ import {
flatten, flatten,
concat, concat,
times, times,
merge,
} from 'lodash'; } from 'lodash';
import React, { import React, {
createContext, createContext,
@ -18,6 +19,7 @@ import React, {
} from 'react'; } from 'react';
import DatabaseContext from './DatabaseContext'; import DatabaseContext from './DatabaseContext';
import initialState from '../data/initialState'; import initialState from '../data/initialState';
import demoState from '../data/demoState.json';
const ResumeContext = createContext({}); const ResumeContext = createContext({});
@ -117,7 +119,20 @@ const ResumeProvider = ({ children }) => {
return newState; return newState;
case 'set_data': case 'set_data':
return payload; newState = payload;
debouncedUpdateResume(newState);
return newState;
case 'reset_data':
newState = merge(clone(state), initialState);
debouncedUpdateResume(newState);
return newState;
case 'load_demo_data':
newState = merge(clone(state), demoState);
newState.metadata.layout = demoState.metadata.layout;
debouncedUpdateResume(newState);
return newState;
default: default:
throw new Error(); throw new Error();

View File

@ -20,6 +20,10 @@ const StorageProvider = ({ children }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const uploadPhotograph = async (file) => { const uploadPhotograph = async (file) => {
if (!file) {
return null;
}
if (!isFileImage(file)) { if (!isFileImage(file)) {
toast.error( toast.error(
"You tried to upload a file that was not an image. That won't look good on your resume. Please try again.", "You tried to upload a file that was not an image. That won't look good on your resume. Please try again.",
@ -27,6 +31,13 @@ const StorageProvider = ({ children }) => {
return null; return null;
} }
if (file.size > 2097152) {
toast.error(
"Your image seems to be bigger than 2 MB. That's way too much. Maybe consider reducing it's size?",
);
return null;
}
const uploadTask = firebase const uploadTask = firebase
.storage() .storage()
.ref(`/users/${user.uid}/photographs/${id}`) .ref(`/users/${user.uid}/photographs/${id}`)

View File

@ -28,7 +28,7 @@ const COLOR_CONFIG = {
}; };
const defaultState = { const defaultState = {
darkMode: false, darkMode: true,
toggleDarkMode: () => {}, toggleDarkMode: () => {},
}; };

View File

@ -1,3 +1,4 @@
import { navigate } from '@reach/router';
import firebase from 'gatsby-plugin-firebase'; import firebase from 'gatsby-plugin-firebase';
import { pick } from 'lodash'; import { pick } from 'lodash';
import React, { createContext, memo, useEffect, useState } from 'react'; import React, { createContext, memo, useEffect, useState } from 'react';
@ -16,6 +17,7 @@ const defaultState = {
user: defaultUser, user: defaultUser,
logout: async () => {}, logout: async () => {},
loginWithGoogle: async () => {}, loginWithGoogle: async () => {},
deleteAccount: async () => {},
}; };
const UserContext = createContext(defaultState); const UserContext = createContext(defaultState);
@ -49,16 +51,33 @@ const UserProvider = ({ children }) => {
const provider = new firebase.auth.GoogleAuthProvider(); const provider = new firebase.auth.GoogleAuthProvider();
try { try {
await firebase.auth().signInWithPopup(provider); return await firebase.auth().signInWithPopup(provider);
} catch (error) { } catch (error) {
toast.error(error.message); toast.error(error.message);
} }
}; };
const logout = async () => { const logout = () => {
await firebase.auth().signOut(); firebase.auth().signOut();
localStorage.removeItem('user'); localStorage.removeItem('user');
setUser(null); setUser(null);
navigate('/');
};
const deleteAccount = async () => {
const { currentUser } = firebase.auth();
try {
await currentUser.delete();
} catch (e) {
toast.error(e.message);
await loginWithGoogle();
await currentUser.delete();
} finally {
logout();
toast(
"It's sad to see you go, but we respect your privacy. All your data has been deleted successfully. Hope to see you again soon!",
);
}
}; };
return ( return (
@ -68,6 +87,7 @@ const UserProvider = ({ children }) => {
logout, logout,
loading, loading,
loginWithGoogle, loginWithGoogle,
deleteAccount,
}} }}
> >
{children} {children}

256
src/data/demoState.json Normal file
View File

@ -0,0 +1,256 @@
{
"awards": {
"heading": "Awards",
"items": [
{
"awarder": "Google",
"date": "2019-04-01",
"id": "6f857f2b-6312-4a0d-907d-2e17991954eb",
"summary": "",
"title": "International Flutter Hackathon '19"
},
{
"awarder": "Venturesity",
"date": "2016-06-01",
"id": "f6efa3f9-9741-4e36-a538-ba0d9779bc61",
"summary": "",
"title": "Venturesity Banyan Hack '16"
}
],
"visible": true
},
"certifications": {
"heading": "Certifications",
"items": [
{
"date": "2018-02-01",
"id": "d2ec12bc-7876-46bc-afd4-11ae06faf3bd",
"issuer": "Google",
"summary": "",
"title": "Applied CS with Android"
},
{
"date": "2019-06-01",
"id": "f8312288-53ae-4504-a768-4b67aea95926",
"issuer": "Udemy",
"summary": "",
"title": "Data Science & Machine Learning using Python"
}
],
"visible": true
},
"education": {
"heading": "Education",
"items": [
{
"degree": "Bachelor's Degree",
"endDate": "2018-04-01",
"field": "Computer Science & Engineering",
"gpa": "9.2",
"id": "c42e2a5a-3f0d-497e-838b-ac2019dcf045",
"institution": "Dayananda Sagar College of Engineering",
"startDate": "2015-04-01",
"summary": ""
},
{
"degree": "Diploma",
"endDate": "2015-04-01",
"field": "Computer Science",
"gpa": "9.8",
"id": "278490a2-c327-4e83-8be8-adf913a9b36c",
"institution": "Dayananda Sagar Institute of Technology",
"startDate": "2012-04-01",
"summary": ""
}
],
"visible": true
},
"hobbies": {
"heading": "Hobbies",
"items": [
{ "id": "92c35e3b-6cd7-4cea-b505-61347ec61b68", "name": "Photography" },
{
"id": "d36f2089-93a9-4f30-a425-3dd81c6b89df",
"name": "Playing Badminton"
},
{
"id": "d1da41a9-ae83-48fb-8047-d45ebd869a69",
"name": "Working on Personal Projects"
}
],
"visible": true
},
"languages": {
"heading": "Languages",
"items": [
{
"fluency": "Very Fluent",
"id": "78d8cf32-84c7-431d-969b-fdf277968026",
"name": "English"
},
{
"fluency": "Native Tongue",
"id": "9e0bd5ed-b88d-4046-8fb9-ecba54d29924",
"name": "Tamil"
},
{
"fluency": "Native Tongue",
"id": "cb895aa9-c485-4bf3-a9e3-08e8f219451a",
"name": "Kannada"
},
{
"fluency": "Learning on Duolingo",
"id": "8fff60fc-0cd6-47e2-b64f-fb249d1af0d1",
"name": "German"
}
],
"visible": true
},
"metadata": {
"colors": {
"background": "#FFFFFF",
"primary": "#009688",
"text": "#212121"
},
"font": "Open Sans",
"layout": [
["objective", "work", "education", "projects"],
["hobbies", "languages"],
["skills", "certifications", "awards", "references"]
],
"template": "onyx"
},
"objective": {
"body": "I'm Amruth Pillai, and as you might have already read, I'm a designer, developer, photographer and a writer. This website was made to showcase all of what I can do and plan to do. Don't judge my writing based on this section though, this is by far my shoddiest work yet.\n&nbsp;\nI got into design because I consider myself a pseudo-perfectionist, if that's even a word? As in, I hate to see things 'not look good'. So I set out on a journey to make products that people use that 'look great', and I'm forever on that path.",
"heading": "Objective",
"visible": true
},
"profile": {
"address": {
"city": "Bangalore, India -",
"line1": "#5/A, Banashankari Nivas,",
"line2": "Brindavan Layout, Subramanyapura,",
"pincode": "560061"
},
"email": "hello@amruthpillai.com",
"firstName": "Amruth",
"heading": "Profile",
"lastName": "Pillai",
"phone": "+91 98453 36113",
"photograph": "https://firebasestorage.googleapis.com/v0/b/rx-resume.appspot.com/o/users%2FNriQrOfocnfTtoRIpR3qEtHNxYq1%2Fphotographs%2Fx7vvg8?alt=media&token=99df9c05-f5e1-4360-b1e8-5c13ddd8cd84",
"profile": "",
"subtitle": "Full Stack Web Developer",
"website": "amruthpillai.com"
},
"projects": {
"heading": "Projects",
"items": [
{
"date": "2020-07-01",
"id": "c768dcca-90f5-4242-a608-6759b4f667fb",
"link": "https://github.com/AmruthPillai/Reactive-Resume",
"summary": "Reactive Resume, a free and open-source resume builder that works for you. A few of the important features that make it awesome are minimalistic UI/UX, extensive customizability, portability, regularly updated templates, etc.\n\nFor more information, check out [rxresu.me](https://github.com/AmruthPillai/Reactive-Resume)",
"title": "Reactive Resume"
},
{
"date": "2020-04-01",
"id": "6ca600b1-c21f-4d7b-8431-f7144d537dd3",
"link": "https://amruthpillai.com",
"summary": "Resume on the Web has been a project that I've been focused on since the early 2014s. I didn't want my information to be displayed on just a sheet of paper that only HRs or Talent Scouts had the privilege of reading, I wanted it to be accessible to everyone. And that's how this project was conceptualized.",
"title": "Resume on the Web"
}
],
"visible": true
},
"public": true,
"references": {
"heading": "References",
"items": [
{
"email": "willywonka@goldenticket.com",
"id": "168339fd-3c4b-4f2f-bd3a-ef184be81700",
"name": "Willy Wonka",
"phone": "+1 (802) 234-2398",
"position": "CEO at Chocolate Factory",
"summary": ""
},
{
"email": "elanmusk@nottesla.com",
"id": "350465b9-9989-43cc-b97e-4115b8980304",
"name": "Elangovan Musk",
"phone": "+91 93893 34353",
"position": "CEO at Newton Motors",
"summary": ""
}
],
"visible": true
},
"skills": {
"heading": "Skills",
"items": [
{
"id": "54e5bceb-d0e9-4f04-98d1-48a34f7cf920",
"level": "Advanced",
"name": "ReactJS"
},
{
"id": "f0274f62-2252-4cc0-bf12-9e1070942c50",
"level": "Advanced",
"name": "Angular"
},
{
"id": "689e2852-df1b-4d41-bda8-c41c88196264",
"level": "Advanced",
"name": "Flutter"
},
{
"id": "3a4f73b1-50c1-4a85-a4b0-2a55dfe5053a",
"level": "Novice",
"name": "Machine Learning"
}
],
"visible": true
},
"social": {
"heading": "Social",
"items": [
{
"id": "a72107fa-a4a5-407d-9e85-39bdb9c0b11a",
"network": "Twitter",
"url": "https://pillai.xyz/twitter",
"username": "KingOKings"
},
{
"id": "1dd46fdd-b3a3-4786-89ce-2e77c0823aba",
"network": "LinkedIn",
"url": "https://pillai.xyz/linkedin",
"username": "AmruthPillai"
}
],
"visible": true
},
"work": {
"heading": "Work Experience",
"items": [
{
"company": "Postdot Technologies Pvt. Ltd.",
"endDate": "",
"id": "d7c64937-0cb9-41b1-a3a6-0679c882fe63",
"position": "Full Stack Web Developer",
"startDate": "2020-06-08",
"summary": "Postman is a great tool when trying to dissect RESTful APIs made by others or test ones you have made yourself. It offers a sleek user interface with which to make HTML requests, without the hassle of writing a bunch of code just to test an API's functionality.",
"website": "https://postman.com"
},
{
"company": "GoDhiyo Solutions Pvt. Ltd.",
"endDate": "2020-04-01",
"id": "f5c5dcfe-2a60-4169-a2f1-b305355518ea",
"position": "Full Stack Web Developer",
"startDate": "2018-07-01",
"summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi laoreet volutpat lacus, sed tempor lacus eleifend feugiat. Pellentesque molestie libero ac varius finibus. Fusce convallis, arcu sit amet lacinia vehicula, nisl justo egestas tortor.\n\n- In vestibulum eros a enim rhoncus\n- Phasellus ullamcorper magna quis est sagittis",
"website": "https://dhiyo.ai"
}
],
"visible": true
}
}

View File

@ -1,7 +1,6 @@
import leftSections from './leftSections'; import leftSections from './leftSections';
const initialState = { const initialState = {
id: '',
profile: { profile: {
heading: 'Profile', heading: 'Profile',
photograph: '', photograph: '',
@ -38,6 +37,11 @@ const initialState = {
visible: true, visible: true,
items: [], items: [],
}, },
projects: {
heading: 'Projects',
visible: true,
items: [],
},
awards: { awards: {
heading: 'Awards', heading: 'Awards',
visible: true, visible: true,
@ -68,7 +72,6 @@ const initialState = {
visible: true, visible: true,
items: [], items: [],
}, },
name: '',
metadata: { metadata: {
template: 'onyx', template: 'onyx',
font: 'Montserrat', font: 'Montserrat',
@ -79,7 +82,7 @@ const initialState = {
background: '#FFFFFF', background: '#FFFFFF',
}, },
}, },
createdAt: new Date(), public: true,
updatedAt: new Date(), updatedAt: new Date(),
}; };

View File

@ -1,6 +1,6 @@
import { AiFillSafetyCertificate, AiOutlineTwitter } from 'react-icons/ai'; import { AiFillSafetyCertificate, AiOutlineTwitter } from 'react-icons/ai';
import { BsTools } from 'react-icons/bs'; import { BsTools } from 'react-icons/bs';
import { FaAward, FaUserFriends } from 'react-icons/fa'; import { FaAward, FaUserFriends, FaProjectDiagram } from 'react-icons/fa';
import { import {
IoLogoGameControllerB, IoLogoGameControllerB,
IoMdBriefcase, IoMdBriefcase,
@ -40,6 +40,12 @@ export default [
icon: MdSchool, icon: MdSchool,
event: ModalEvents.EDUCATION_MODAL, event: ModalEvents.EDUCATION_MODAL,
}, },
{
id: 'projects',
name: 'Projects',
icon: FaProjectDiagram,
event: ModalEvents.PROJECT_MODAL,
},
{ {
id: 'awards', id: 'awards',
name: 'Awards', name: 'Awards',

View File

@ -60,7 +60,7 @@ const AuthModal = () => {
state={[open, setOpen]} state={[open, setOpen]}
action={user ? loggedInAction : loggedOutAction} action={user ? loggedInAction : loggedOutAction}
> >
<p>{getMessage()}</p> <p className="leading-loose">{getMessage()}</p>
</BaseModal> </BaseModal>
); );
}; };

View File

@ -7,6 +7,7 @@ import EducationModal from './sections/EducationModal';
import HobbyModal from './sections/HobbyModal'; import HobbyModal from './sections/HobbyModal';
import ImportModal from './sections/ImportModal'; import ImportModal from './sections/ImportModal';
import LanguageModal from './sections/LanguageModal'; import LanguageModal from './sections/LanguageModal';
import ProjectModal from './sections/ProjectModal';
import ReferenceModal from './sections/ReferenceModal'; import ReferenceModal from './sections/ReferenceModal';
import SkillModal from './sections/SkillModal'; import SkillModal from './sections/SkillModal';
import SocialModal from './sections/SocialModal'; import SocialModal from './sections/SocialModal';
@ -20,6 +21,7 @@ const ModalRegistrar = () => {
<SocialModal /> <SocialModal />
<WorkModal /> <WorkModal />
<EducationModal /> <EducationModal />
<ProjectModal />
<AwardModal /> <AwardModal />
<CertificateModal /> <CertificateModal />
<SkillModal /> <SkillModal />

View File

@ -55,7 +55,7 @@ const ResumeModal = () => {
{...getFieldProps(formik, schema, 'name')} {...getFieldProps(formik, schema, 'name')}
/> />
<p> <p className="leading-loose">
You are going to be creating a new resume from scratch, but first, You are going to be creating a new resume from scratch, but first,
let&apos;s give it a name. This can be the name of the role you want let&apos;s give it a name. This can be the name of the role you want
to apply for, or if you&apos;re making a resume for a friend, you to apply for, or if you&apos;re making a resume for a friend, you

View File

@ -1,8 +1,7 @@
import { FieldArray, Formik } from 'formik'; import { Formik } from 'formik';
import React, { memo } from 'react'; import React, { memo } from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import Input from '../../components/shared/Input'; import Input from '../../components/shared/Input';
import InputArray from '../../components/shared/InputArray';
import ModalEvents from '../../constants/ModalEvents'; import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils'; import { getFieldProps } from '../../utils';
import DataModal from '../DataModal'; import DataModal from '../DataModal';
@ -14,8 +13,7 @@ const initialValues = {
gpa: '', gpa: '',
startDate: '', startDate: '',
endDate: '', endDate: '',
courses: [], summary: '',
temp: '',
}; };
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@ -30,8 +28,7 @@ const schema = Yup.object().shape({
startDate && startDate &&
yupSchema.min(startDate, 'End Date must be later than Start Date'), yupSchema.min(startDate, 'End Date must be later than Start Date'),
), ),
courses: Yup.array().of(Yup.string().required('This is a required field.')), summary: Yup.string().min(10, 'Please enter at least 10 characters.'),
temp: Yup.string().ensure(),
}); });
const EducationModal = () => { const EducationModal = () => {
@ -88,18 +85,11 @@ const EducationModal = () => {
{...getFieldProps(formik, schema, 'endDate')} {...getFieldProps(formik, schema, 'endDate')}
/> />
<FieldArray <Input
name="courses" type="textarea"
render={(helpers) => ( label="Summary"
<InputArray className="col-span-2"
formik={formik} {...getFieldProps(formik, schema, 'summary')}
schema={schema}
helpers={helpers}
label="Courses"
path="courses"
placeholder="Data Structures &amp; Algortihms"
/>
)}
/> />
</div> </div>
</DataModal> </DataModal>

View File

@ -0,0 +1,69 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import * as Yup from 'yup';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils';
import DataModal from '../DataModal';
const initialValues = {
title: '',
link: '',
date: '',
summary: '',
};
const schema = Yup.object().shape({
title: Yup.string().required('This is a required field.'),
link: Yup.string().url('Must be a valid URL'),
date: Yup.date().max(new Date()),
summary: Yup.string(),
});
const ProjectModal = () => {
return (
<Formik
validateOnBlur
initialValues={initialValues}
validationSchema={schema}
>
{(formik) => (
<DataModal
name="Project"
path="projects.items"
event={ModalEvents.PROJECT_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Title"
className="col-span-2"
placeholder="Reactive Resume"
{...getFieldProps(formik, schema, 'title')}
/>
<Input
label="Link"
placeholder="https://github.com/AmruthPillai/Reactive-Resume"
{...getFieldProps(formik, schema, 'link')}
/>
<Input
type="date"
label="Date"
{...getFieldProps(formik, schema, 'date')}
/>
<Input
type="textarea"
label="Summary"
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>
</div>
</DataModal>
)}
</Formik>
);
};
export default memo(ProjectModal);

View File

@ -1,8 +1,7 @@
import { FieldArray, Formik } from 'formik'; import { Formik } from 'formik';
import React, { memo } from 'react'; import React, { memo } from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import Input from '../../components/shared/Input'; import Input from '../../components/shared/Input';
import InputArray from '../../components/shared/InputArray';
import ModalEvents from '../../constants/ModalEvents'; import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils'; import { getFieldProps } from '../../utils';
import DataModal from '../DataModal'; import DataModal from '../DataModal';
@ -14,8 +13,6 @@ const initialValues = {
startDate: '', startDate: '',
endDate: '', endDate: '',
summary: '', summary: '',
highlights: [],
temp: '',
}; };
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@ -30,10 +27,6 @@ const schema = Yup.object().shape({
yupSchema.min(startDate, 'End Date must be later than Start Date'), yupSchema.min(startDate, 'End Date must be later than Start Date'),
), ),
summary: Yup.string().min(10, 'Please enter at least 10 characters.'), summary: Yup.string().min(10, 'Please enter at least 10 characters.'),
highlights: Yup.array().of(
Yup.string().required('This is a required field.'),
),
temp: Yup.string().ensure(),
}); });
const WorkModal = () => { const WorkModal = () => {
@ -89,20 +82,6 @@ const WorkModal = () => {
className="col-span-2" className="col-span-2"
{...getFieldProps(formik, schema, 'summary')} {...getFieldProps(formik, schema, 'summary')}
/> />
<FieldArray
name="highlights"
render={(helpers) => (
<InputArray
formik={formik}
schema={schema}
helpers={helpers}
label="Highlights"
path="highlights"
placeholder="Worked passionately in customer service in a high volume restaurant."
/>
)}
/>
</div> </div>
</DataModal> </DataModal>
)} )}

View File

@ -1,5 +1,5 @@
import firebase from 'gatsby-plugin-firebase'; import firebase from 'gatsby-plugin-firebase';
import React, { memo, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import CreateResume from '../../components/dashboard/CreateResume'; import CreateResume from '../../components/dashboard/CreateResume';
import ResumePreview from '../../components/dashboard/ResumePreview'; import ResumePreview from '../../components/dashboard/ResumePreview';
@ -11,11 +11,24 @@ const Dashboard = ({ user }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const ref = `users/${user.uid}/resumes`; const resumesRef = 'resumes';
const socketRef = '/.info/connected';
firebase firebase
.database() .database()
.ref(ref) .ref(socketRef)
.on('value', (snapshot) => {
if (snapshot.val()) {
setLoading(false);
firebase.database().ref(socketRef).off();
}
});
firebase
.database()
.ref(resumesRef)
.orderByChild('user')
.equalTo(user.uid)
.on('value', (snapshot) => { .on('value', (snapshot) => {
if (snapshot.val()) { if (snapshot.val()) {
const resumesArr = []; const resumesArr = [];
@ -23,13 +36,13 @@ const Dashboard = ({ user }) => {
Object.keys(data).forEach((key) => resumesArr.push(data[key])); Object.keys(data).forEach((key) => resumesArr.push(data[key]));
setResumes(resumesArr); setResumes(resumesArr);
} }
setLoading(false);
}); });
firebase firebase
.database() .database()
.ref(ref) .ref(resumesRef)
.orderByChild('user')
.equalTo(user.uid)
.on('child_removed', (snapshot) => { .on('child_removed', (snapshot) => {
if (snapshot.val()) { if (snapshot.val()) {
setResumes(resumes.filter((x) => x.id === snapshot.val().id)); setResumes(resumes.filter((x) => x.id === snapshot.val().id));
@ -37,7 +50,7 @@ const Dashboard = ({ user }) => {
}); });
return () => { return () => {
firebase.database().ref(ref).off(); firebase.database().ref(resumesRef).off();
}; };
}, [user]); }, [user]);
@ -67,4 +80,4 @@ const Dashboard = ({ user }) => {
); );
}; };
export default memo(Dashboard); export default Dashboard;

View File

@ -65,7 +65,7 @@ const Feature = ({ title, children }) => {
return ( return (
<div className="mt-16"> <div className="mt-16">
<h3 className="text-3xl">{title}</h3> <h3 className="text-3xl">{title}</h3>
<p className="mt-6 text-lg">{children}</p> <p className="mt-6 text-lg leading-loose">{children}</p>
</div> </div>
); );
}; };

17
src/pages/r.js Normal file
View File

@ -0,0 +1,17 @@
import { Redirect, Router } from '@reach/router';
import React, { memo } from 'react';
import Wrapper from '../components/shared/Wrapper';
import ResumeViewer from './r/view';
import NotFound from './404';
const ResumeRouter = () => (
<Wrapper>
<Router>
<Redirect noThrow from="/r" to="/" exact />
<ResumeViewer path="r/:id" />
<NotFound default />
</Router>
</Wrapper>
);
export default memo(ResumeRouter);

58
src/pages/r/view.js Normal file
View File

@ -0,0 +1,58 @@
import { navigate, Link } from '@reach/router';
import React, { memo, useContext, useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet';
import { toast } from 'react-toastify';
import LoadingScreen from '../../components/router/LoadingScreen';
import DatabaseContext from '../../contexts/DatabaseContext';
import Onyx from '../../templates/Onyx';
import styles from './view.module.css';
const ResumeViewer = ({ id }) => {
const [resume, setResume] = useState(null);
const [loading, setLoading] = useState(true);
const { getResume } = useContext(DatabaseContext);
useEffect(() => {
(async () => {
const data = await getResume(id);
if (!data) {
navigate('/');
toast.error(
`The resume you were looking for does not exist anymore... or maybe it never did?`,
);
return null;
}
setResume(data);
return setLoading(false);
})();
}, [id]);
return useMemo(() => {
if (loading) {
return <LoadingScreen />;
}
return (
<div className={styles.container}>
<Helmet>
<title>{resume.name} | Reactive Resume</title>
<link rel="canonical" href={`https://rxresu.me/r/${id}`} />
</Helmet>
<div
className={styles.page}
style={{ backgroundColor: resume.metadata.colors.background }}
>
{resume.metadata.template === 'onyx' && <Onyx data={resume} />}
</div>
<p className={styles.footer}>
Built with <Link to="/">Reactive Resume</Link>
</p>
</div>
);
});
};
export default memo(ResumeViewer);

View File

@ -0,0 +1,13 @@
.container {
background-color: #212121;
@apply h-screen overflow-scroll col-span-5 flex flex-col items-center;
}
.page {
width: 800px;
@apply block my-16 rounded shadow-2xl;
}
.footer {
@apply mb-16 text-center opacity-50 leading-loose;
}

View File

@ -6,6 +6,10 @@ body {
@apply transition-colors duration-200 ease-in-out; @apply transition-colors duration-200 ease-in-out;
} }
a {
@apply font-semibold;
}
a:hover { a:hover {
@apply underline; @apply underline;
} }
@ -27,7 +31,7 @@ label {
} }
label > span:first-child { label > span:first-child {
@apply mb-1 text-primary-500 font-semibold tracking-wide text-xs uppercase; @apply mb-1 text-primary-600 font-semibold tracking-wide text-xs uppercase;
} }
.MuiTooltip-tooltip { .MuiTooltip-tooltip {
@ -47,3 +51,7 @@ label > span:first-child {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.markdown {
@apply leading-relaxed whitespace-pre-wrap;
}

View File

@ -9,6 +9,7 @@ import Heading from './blocks/Heading/HeadingA';
import HobbiesA from './blocks/Hobbies/HobbiesA'; import HobbiesA from './blocks/Hobbies/HobbiesA';
import LanguagesA from './blocks/Languages/LanguagesA'; import LanguagesA from './blocks/Languages/LanguagesA';
import ObjectiveA from './blocks/Objective/ObjectiveA'; import ObjectiveA from './blocks/Objective/ObjectiveA';
import ProjectsA from './blocks/Projects/ProjectsA';
import ReferencesA from './blocks/References/ReferencesA'; import ReferencesA from './blocks/References/ReferencesA';
import SkillsA from './blocks/Skills/SkillsA'; import SkillsA from './blocks/Skills/SkillsA';
import WorkA from './blocks/Work/WorkA'; import WorkA from './blocks/Work/WorkA';
@ -17,6 +18,7 @@ const Blocks = {
objective: ObjectiveA, objective: ObjectiveA,
work: WorkA, work: WorkA,
education: EducationA, education: EducationA,
projects: ProjectsA,
awards: AwardsA, awards: AwardsA,
certifications: CertificationsA, certifications: CertificationsA,
skills: SkillsA, skills: SkillsA,
@ -35,8 +37,7 @@ const Onyx = ({ data }) => {
return ( return (
<PageContext.Provider value={{ data, heading: Heading }}> <PageContext.Provider value={{ data, heading: Heading }}>
<div <div
id="page" className="p-10 rounded"
className="p-10"
style={{ style={{
fontFamily: data.metadata.font, fontFamily: data.metadata.font,
color: data.metadata.colors.text, color: data.metadata.colors.text,
@ -45,12 +46,14 @@ const Onyx = ({ data }) => {
> >
<div className="grid grid-cols-4 items-center"> <div className="grid grid-cols-4 items-center">
<div className="col-span-3 flex items-center"> <div className="col-span-3 flex items-center">
<img {data.profile.photograph && (
className="rounded object-cover mr-4" <img
src={data.profile.photograph} className="rounded object-cover mr-4"
alt="Resume Photograph" src={data.profile.photograph}
style={{ width: '120px', height: '120px' }} alt="Resume Photograph"
/> style={{ width: '120px', height: '120px' }}
/>
)}
<div> <div>
<h1 <h1
@ -75,18 +78,18 @@ const Onyx = ({ data }) => {
</div> </div>
<hr <hr
className="my-6 opacity-25" className="my-5 opacity-25"
style={{ borderColor: data.metadata.colors.text }} style={{ borderColor: data.metadata.colors.text }}
/> />
<div className="grid grid-cols-1 col-gap-8"> <div className="grid gap-4">
{data.metadata.layout[0] && {data.metadata.layout[0] &&
data.metadata.layout[0].map((x) => { data.metadata.layout[0].map((x) => {
const Component = Blocks[x]; const Component = Blocks[x];
return Component && <Component key={x} />; return Component && <Component key={x} />;
})} })}
<div className="grid grid-cols-3 col-gap-8"> <div className="grid grid-cols-2 gap-4">
{data.metadata.layout[1] && {data.metadata.layout[1] &&
data.metadata.layout[1].map((x) => { data.metadata.layout[1].map((x) => {
const Component = Blocks[x]; const Component = Blocks[x];

View File

@ -2,30 +2,32 @@ import moment from 'moment';
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const AwardItem = (x) => ( const AwardItem = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id}>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div className="flex flex-col">
<h6 className="font-semibold">{x.title}</h6> <h6 className="font-semibold">{x.title}</h6>
<p className="text-xs">{x.awarder}</p> <span className="text-xs">{x.awarder}</span>
</div> </div>
{x.date && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium">
{moment(x.date).format('MMMM YYYY')} {moment(x.date).format('MMMM YYYY')}
</h6> </h6>
)}
</div> </div>
<ReactMarkdown className="mt-2 text-sm" source={x.summary} /> <ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
</div> </div>
); );
const AwardsA = () => { const AwardsA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.awards.visible && data.awards.items ? ( return safetyCheck(data.awards) ? (
<div> <div>
<Heading>{data.awards.heading}</Heading> <Heading>{data.awards.heading}</Heading>
{data.awards.items.map(AwardItem)} <div className="grid gap-4">{data.awards.items.map(AwardItem)}</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -2,30 +2,34 @@ import moment from 'moment';
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const CertificationItem = (x) => ( const CertificationItem = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id}>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div className="flex flex-col">
<h6 className="font-semibold">{x.title}</h6> <h6 className="font-semibold">{x.title}</h6>
<p className="text-xs">{x.issuer}</p> <span className="text-xs">{x.issuer}</span>
</div> </div>
{x.date && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium">
{moment(x.date).format('MMMM YYYY')} {moment(x.date).format('MMMM YYYY')}
</h6> </h6>
)}
</div> </div>
<ReactMarkdown className="mt-2 text-sm" source={x.summary} /> <ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
</div> </div>
); );
const CertificationsA = () => { const CertificationsA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.certifications.visible && data.certifications.items ? ( return safetyCheck(data.certifications) ? (
<div> <div>
<Heading>{data.certifications.heading}</Heading> <Heading>{data.certifications.heading}</Heading>
{data.certifications.items.map(CertificationItem)} <div className="grid gap-4">
{data.certifications.items.map(CertificationItem)}
</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -2,6 +2,7 @@ import { get } from 'lodash';
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import { FaCaretRight } from 'react-icons/fa'; import { FaCaretRight } from 'react-icons/fa';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
import Icons from '../Icons'; import Icons from '../Icons';
const ContactItem = ({ value, icon, link }) => { const ContactItem = ({ value, icon, link }) => {
@ -9,9 +10,9 @@ const ContactItem = ({ value, icon, link }) => {
const Icon = get(Icons, icon.toLowerCase(), FaCaretRight); const Icon = get(Icons, icon.toLowerCase(), FaCaretRight);
return value ? ( return value ? (
<div className="flex items-center my-3"> <div className="flex items-center">
<Icon <Icon
size="14px" size="10px"
className="mr-2" className="mr-2"
style={{ color: data.metadata.colors.primary }} style={{ color: data.metadata.colors.primary }}
/> />
@ -30,7 +31,7 @@ const ContactA = () => {
const { data } = useContext(PageContext); const { data } = useContext(PageContext);
return ( return (
<div className="col-span-1 text-xs"> <div className="text-xs grid gap-2">
<ContactItem <ContactItem
icon="phone" icon="phone"
value={data.profile.phone} value={data.profile.phone}
@ -47,18 +48,15 @@ const ContactA = () => {
link={`mailto:${data.profile.email}`} link={`mailto:${data.profile.email}`}
/> />
{data.social.visible && data.social.items ? ( {safetyCheck(data.social) &&
<div> data.social.items.map((x) => (
{data.social.items.map((x) => ( <ContactItem
<ContactItem key={x.id}
key={x.id} value={x.username}
value={x.username} icon={x.network}
icon={x.network} link={x.url}
link={x.url} />
/> ))}
))}
</div>
) : null}
</div> </div>
); );
}; };

View File

@ -1,40 +1,39 @@
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { formatDateRange } from '../../../utils'; import { formatDateRange, safetyCheck } from '../../../utils';
const EducationItem = (x) => ( const EducationItem = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id}>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div className="flex flex-col">
<h6 className="font-semibold">{x.institution}</h6> <h6 className="font-semibold">{x.institution}</h6>
<span className="text-xs"> <span className="text-xs">
<strong>{x.degree}</strong> {x.field} <strong>{x.degree}</strong> {x.field}
</span> </span>
</div> </div>
<div className="flex flex-col items-end"> <div className="flex flex-col items-end">
<span className="text-xs font-medium"> {x.startDate && (
({formatDateRange({ startDate: x.startDate, endDate: x.endDate })}) <h6 className="text-xs font-medium">
</span> ({formatDateRange({ startDate: x.startDate, endDate: x.endDate })})
<h6 className="text-sm font-medium">{x.gpa}</h6> </h6>
)}
<span className="text-sm font-medium">{x.gpa}</span>
</div> </div>
</div> </div>
{x.courses && ( <ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
<ul className="mt-2 text-sm list-disc list-inside">
{x.courses.map((y) => (
<li key={y}>{y}</li>
))}
</ul>
)}
</div> </div>
); );
const EducationA = () => { const EducationA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.education.visible && data.education.items ? ( return safetyCheck(data.education) ? (
<div> <div>
<Heading>{data.education.heading}</Heading> <Heading>{data.education.heading}</Heading>
{data.education.items.map(EducationItem)} <div className="grid gap-4">
{data.education.items.map(EducationItem)}
</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -1,4 +1,4 @@
import React, { useContext, memo } from 'react'; import React, { memo, useContext } from 'react';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
const HeadingA = ({ children }) => { const HeadingA = ({ children }) => {
@ -6,7 +6,7 @@ const HeadingA = ({ children }) => {
return ( return (
<h6 <h6
className="text-xs font-bold uppercase mt-4 mb-1" className="text-xs font-bold uppercase mb-1"
style={{ color: data.metadata.colors.primary }} style={{ color: data.metadata.colors.primary }}
> >
{children} {children}

View File

@ -1,8 +1,9 @@
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const HobbyA = (x) => ( const HobbyA = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id}>
<h6 className="font-semibold">{x.name}</h6> <h6 className="font-semibold">{x.name}</h6>
</div> </div>
); );
@ -10,10 +11,10 @@ const HobbyA = (x) => (
const HobbiesA = () => { const HobbiesA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.hobbies.visible && data.hobbies.items ? ( return safetyCheck(data.hobbies) ? (
<div> <div>
<Heading>{data.hobbies.heading}</Heading> <Heading>{data.hobbies.heading}</Heading>
{data.hobbies.items.map(HobbyA)} <div className="grid gap-2">{data.hobbies.items.map(HobbyA)}</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -1,20 +1,23 @@
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const LanguageItem = (x) => ( const LanguageItem = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id} className="flex flex-col">
<h6 className="font-semibold">{x.name}</h6> <h6 className="font-semibold">{x.name}</h6>
<p className="text-xs">{x.fluency}</p> <span className="text-xs">{x.fluency}</span>
</div> </div>
); );
const LanguagesA = () => { const LanguagesA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.languages.visible && data.languages.items ? ( return safetyCheck(data.languages) ? (
<div> <div>
<Heading>{data.languages.heading}</Heading> <Heading>{data.languages.heading}</Heading>
{data.languages.items.map(LanguageItem)} <div className="grid grid-cols-2 gap-2">
{data.languages.items.map(LanguageItem)}
</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -1,15 +1,21 @@
import React, { useContext, memo } from 'react'; import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const ObjectiveA = () => { const ObjectiveA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return ( return (
<div> safetyCheck(data.objective, 'body') && (
<Heading>{data.objective.heading}</Heading> <div>
<ReactMarkdown className="text-sm" source={data.objective.body} /> <Heading>{data.objective.heading}</Heading>
</div> <ReactMarkdown
className="markdown text-sm"
source={data.objective.body}
/>
</div>
)
); );
}; };

View File

@ -0,0 +1,39 @@
import moment from 'moment';
import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const ProjectItem = (x) => (
<div key={x.id}>
<div className="flex justify-between items-center">
<div className="flex flex-col">
<h6 className="font-semibold">{x.title}</h6>
{x.link && (
<a href={x.link} className="text-xs">
{x.link}
</a>
)}
</div>
{x.date && (
<h6 className="text-xs font-medium">
{moment(x.date).format('MMMM YYYY')}
</h6>
)}
</div>
<ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
</div>
);
const ProjectsA = () => {
const { data, heading: Heading } = useContext(PageContext);
return safetyCheck(data.projects) ? (
<div>
<Heading>{data.projects.heading}</Heading>
<div className="grid gap-4">{data.projects.items.map(ProjectItem)}</div>
</div>
) : null;
};
export default memo(ProjectsA);

View File

@ -1,24 +1,25 @@
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const ReferenceItem = (x) => ( const ReferenceItem = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id} className="flex flex-col">
<h6 className="font-semibold">{x.name}</h6> <h6 className="font-semibold">{x.name}</h6>
<p className="text-xs">{x.position}</p> <span className="text-xs">{x.position}</span>
<p className="text-xs">{x.phone}</p> <span className="text-xs">{x.phone}</span>
<p className="text-xs">{x.email}</p> <span className="text-xs">{x.email}</span>
<ReactMarkdown className="mt-2 text-sm" source={x.summary} /> <ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
</div> </div>
); );
const ReferencesA = () => { const ReferencesA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.references.visible && data.references.items ? ( return safetyCheck(data.references) ? (
<div> <div>
<Heading>{data.references.heading}</Heading> <Heading>{data.references.heading}</Heading>
<div className="grid grid-cols-3 col-gap-8"> <div className="grid grid-cols-3 gap-4">
{data.references.items.map(ReferenceItem)} {data.references.items.map(ReferenceItem)}
</div> </div>
</div> </div>

View File

@ -1,20 +1,23 @@
import React, { memo, useContext } from 'react'; import React, { memo, useContext } from 'react';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const SkillItem = (x) => ( const SkillItem = (x) => (
<div key={x.id} className="mb-2"> <div key={x.id} className="flex flex-col">
<h6 className="font-semibold">{x.name}</h6> <h6 className="font-semibold">{x.name}</h6>
<p className="text-xs">{x.level}</p> <span className="text-xs">{x.level}</span>
</div> </div>
); );
const SkillsA = () => { const SkillsA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.skills.visible && data.skills.items ? ( return safetyCheck(data.skills) ? (
<div> <div>
<Heading>{data.skills.heading}</Heading> <Heading>{data.skills.heading}</Heading>
{data.skills.items.map(SkillItem)} <div className="grid grid-cols-2 gap-2">
{data.skills.items.map(SkillItem)}
</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -1,37 +1,32 @@
import React, { useContext, memo } from 'react'; import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext'; import PageContext from '../../../contexts/PageContext';
import { formatDateRange } from '../../../utils'; import { formatDateRange, safetyCheck } from '../../../utils';
const WorkItem = (x) => ( const WorkItem = (x) => (
<div key={x.id} className="mb-4 last:mb-0"> <div key={x.id}>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div className="flex flex-col">
<h6 className="font-semibold">{x.company}</h6> <h6 className="font-semibold">{x.company}</h6>
<span className="text-xs">{x.position}</span> <span className="text-xs">{x.position}</span>
</div> </div>
<span className="text-xs font-medium"> {x.startDate && (
({formatDateRange({ startDate: x.startDate, endDate: x.endDate })}) <h6 className="text-xs font-medium">
</span> ({formatDateRange({ startDate: x.startDate, endDate: x.endDate })})
</h6>
)}
</div> </div>
<ReactMarkdown className="mt-2 text-sm" source={x.summary} /> <ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
{x.highlights && (
<ul className="mt-2 text-sm list-disc list-inside">
{x.highlights.map((y) => (
<li key={y}>{y}</li>
))}
</ul>
)}
</div> </div>
); );
const WorkA = () => { const WorkA = () => {
const { data, heading: Heading } = useContext(PageContext); const { data, heading: Heading } = useContext(PageContext);
return data.work.visible && data.work.items ? ( return safetyCheck(data.work) ? (
<div> <div>
<Heading>{data.work.heading}</Heading> <Heading>{data.work.heading}</Heading>
<div>{data.work.items.map(WorkItem)}</div> <div className="grid gap-4">{data.work.items.map(WorkItem)}</div>
</div> </div>
) : null; ) : null;
}; };

View File

@ -1,14 +1,12 @@
import { get } from 'lodash'; import { get, isEmpty } from 'lodash';
import moment from 'moment'; import moment from 'moment';
export const getModalText = (isEditMode, type) => { export const getModalText = (isEditMode, type) => {
return isEditMode ? `Edit ${type}` : `Add ${type}`; return isEditMode ? `Edit ${type}` : `Add ${type}`;
}; };
export const transformCollectionSnapshot = (snapshot, setData) => { export const safetyCheck = (section, path = 'items') => {
const data = []; return !!(section && section.visible === true && !isEmpty(section[path]));
snapshot.forEach((doc) => data.push(doc.data()));
setData(data);
}; };
export const handleKeyUp = (event, action) => { export const handleKeyUp = (event, action) => {