- implement i18n

- translation dynamic for sections
- added articles for SEO
This commit is contained in:
Amruth Pillai 2020-07-16 08:42:19 +05:30
parent b7c565de79
commit a7657b4a5c
74 changed files with 2373 additions and 586 deletions

View File

@ -4,5 +4,9 @@
},
"editor.codeActionsOnSave": {
"source.fixAll": true
}
},
"i18n-ally.localesPaths": [
"src/i18n/locales"
],
"i18n-ally.keystyle": "nested"
}

View File

@ -10,10 +10,11 @@ import { DatabaseProvider } from './src/contexts/DatabaseContext';
import { ModalProvider } from './src/contexts/ModalContext';
import { ResumeProvider } from './src/contexts/ResumeContext';
import { StorageProvider } from './src/contexts/StorageContext';
import { ThemeProvider } from './src/contexts/ThemeContext';
import { SettingsProvider } from './src/contexts/SettingsContext';
import { UserProvider } from './src/contexts/UserContext';
import './src/styles/global.css';
import './src/i18n';
import './src/styles/forms.css';
import './src/styles/global.css';
import './src/styles/shadows.css';
import './src/styles/tailwind.css';
import './src/styles/toastify.css';
@ -27,7 +28,7 @@ const theme = createMuiTheme({
// eslint-disable-next-line import/prefer-default-export
export const wrapRootElement = ({ element }) => (
<ThemeProvider>
<SettingsProvider>
<MuiThemeProvider theme={theme}>
<ModalProvider>
<UserProvider>
@ -39,5 +40,5 @@ export const wrapRootElement = ({ element }) => (
</UserProvider>
</ModalProvider>
</MuiThemeProvider>
</ThemeProvider>
</SettingsProvider>
);

View File

@ -82,7 +82,7 @@ module.exports = {
{
resolve: 'gatsby-plugin-offline',
options: {
precachePages: ['', '/app/*'],
precachePages: ['', '/app/*', '/blog/*'],
},
},
'gatsby-plugin-lodash',
@ -95,6 +95,14 @@ module.exports = {
},
},
'gatsby-plugin-postcss',
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/articles`,
name: `articles`,
},
},
`gatsby-transformer-remark`,
{
resolve: 'gatsby-source-filesystem',
options: {

View File

@ -25,3 +25,41 @@ exports.onCreatePage = async ({ page, actions }) => {
createPage(page);
}
};
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions;
const blogTemplate = require.resolve(`./src/components/Blog.js`);
const result = await graphql(`
{
allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] }
limit: 1000
) {
edges {
node {
frontmatter {
slug
}
}
}
}
}
`);
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`);
return;
}
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.slug,
component: blogTemplate,
context: {
slug: node.frontmatter.slug,
},
});
});
};

763
package-lock.json generated
View File

@ -2374,6 +2374,24 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
"integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
},
"@types/vfile": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz",
"integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==",
"requires": {
"@types/node": "*",
"@types/unist": "*",
"@types/vfile-message": "*"
}
},
"@types/vfile-message": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz",
"integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==",
"requires": {
"vfile-message": "*"
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
@ -2906,6 +2924,11 @@
"is-string": "^1.0.5"
}
},
"array-iterate": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-1.1.4.tgz",
"integrity": "sha512-sNRaPGh9nnmdC8Zf+pT3UqP8rnWj5Hf9wiFGsX3wUQ2yVSIhO2ShFwCoceIPpB41QF6i2OEmrHmCo36xronCVA=="
},
"array-map": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
@ -5433,6 +5456,11 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
"css-selector-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
"integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g=="
},
"css-selector-tokenizer": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz",
@ -9285,6 +9313,285 @@
}
}
},
"gatsby-transformer-remark": {
"version": "2.8.25",
"resolved": "https://registry.npmjs.org/gatsby-transformer-remark/-/gatsby-transformer-remark-2.8.25.tgz",
"integrity": "sha512-RTO741t6eG4lw0WWmZRjN55SLBx225fJr+t+QkqUNTbBdTxRuUfHyAK6gRzVDtIC9Jtr1fQZxK0Hx2M6nj72RQ==",
"requires": {
"@babel/runtime": "^7.10.3",
"bluebird": "^3.7.2",
"gatsby-core-utils": "^1.3.12",
"gray-matter": "^4.0.2",
"hast-util-raw": "^4.0.0",
"hast-util-to-html": "^4.0.1",
"lodash": "^4.17.15",
"mdast-util-to-hast": "^3.0.4",
"mdast-util-to-string": "^1.1.0",
"mdast-util-toc": "^5.0",
"remark": "^10.0.1",
"remark-parse": "^6.0.3",
"remark-retext": "^3.1.3",
"remark-stringify": "6.0.4",
"retext-english": "^3.0.4",
"sanitize-html": "^1.27.0",
"underscore.string": "^3.3.5",
"unified": "^6.2.0",
"unist-util-remove-position": "^1.1.4",
"unist-util-select": "^1.5.0",
"unist-util-visit": "^1.4.1"
},
"dependencies": {
"hast-to-hyperscript": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-5.0.0.tgz",
"integrity": "sha512-DLl3eYTz8uwwzEubDUdCChsR5t5b2ne+yvHrA2h58Suq/JnN3+Gsb9Tc4iZoCCsykmFUc6UUpwxTmQXs0akSeg==",
"requires": {
"comma-separated-tokens": "^1.0.0",
"property-information": "^4.0.0",
"space-separated-tokens": "^1.0.0",
"style-to-object": "^0.2.1",
"unist-util-is": "^2.0.0",
"web-namespaces": "^1.1.2"
}
},
"hast-util-from-parse5": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-4.0.2.tgz",
"integrity": "sha512-I6dtjsGtDqz4fmGSiFClFyiXdKhj5bPceS6intta7k/VDuiKz9P61C6hO6WMiNNmEm1b/EtBH8f+juvz4o0uwQ==",
"requires": {
"ccount": "^1.0.3",
"hastscript": "^4.0.0",
"property-information": "^4.0.0",
"web-namespaces": "^1.1.2",
"xtend": "^4.0.1"
}
},
"hast-util-raw": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-4.0.0.tgz",
"integrity": "sha512-5xYHyEJMCf8lX/NT4iA5z6N43yoFsrJqXJ5GWwAbLn815URbIz+UNNFEgid33F9paZuDlqVKvB+K3Aqu5+DdSw==",
"requires": {
"hast-util-from-parse5": "^4.0.2",
"hast-util-to-parse5": "^4.0.1",
"html-void-elements": "^1.0.1",
"parse5": "^5.0.0",
"unist-util-position": "^3.0.0",
"web-namespaces": "^1.0.0",
"xtend": "^4.0.1",
"zwitch": "^1.0.0"
}
},
"hast-util-to-parse5": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-4.0.1.tgz",
"integrity": "sha512-U/61W+fsNfBpCyJBB5Pt3l5ypIfgXqEyW9pyrtxF7XrqDJHzcFrYpnC94d0JDYjvobLpYCzcU9srhMRPEO1YXw==",
"requires": {
"hast-to-hyperscript": "^5.0.0",
"property-information": "^4.0.0",
"web-namespaces": "^1.0.0",
"xtend": "^4.0.1",
"zwitch": "^1.0.0"
}
},
"hastscript": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-4.1.0.tgz",
"integrity": "sha512-bOTn9hEfzewvHyXdbYGKqOr/LOz+2zYhKbC17U2YAjd16mnjqB1BQ0nooM/RdMy/htVyli0NAznXiBtwDi1cmQ==",
"requires": {
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.2.0",
"property-information": "^4.0.0",
"space-separated-tokens": "^1.0.0"
}
},
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"markdown-table": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz",
"integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q=="
},
"mdast-util-compact": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz",
"integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==",
"requires": {
"unist-util-visit": "^1.1.0"
}
},
"mdast-util-definitions": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-1.2.5.tgz",
"integrity": "sha512-CJXEdoLfiISCDc2JB6QLb79pYfI6+GcIH+W2ox9nMc7od0Pz+bovcHsiq29xAQY6ayqe/9CsK2VzkSJdg1pFYA==",
"requires": {
"unist-util-visit": "^1.0.0"
}
},
"mdast-util-to-hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-3.0.4.tgz",
"integrity": "sha512-/eIbly2YmyVgpJNo+bFLLMCI1XgolO/Ffowhf+pHDq3X4/V6FntC9sGQCDLM147eTS+uSXv5dRzJyFn+o0tazA==",
"requires": {
"collapse-white-space": "^1.0.0",
"detab": "^2.0.0",
"mdast-util-definitions": "^1.2.0",
"mdurl": "^1.0.1",
"trim": "0.0.1",
"trim-lines": "^1.0.0",
"unist-builder": "^1.0.1",
"unist-util-generated": "^1.1.0",
"unist-util-position": "^3.0.0",
"unist-util-visit": "^1.1.0",
"xtend": "^4.0.1"
}
},
"parse-entities": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz",
"integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==",
"requires": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
}
},
"property-information": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-4.2.0.tgz",
"integrity": "sha512-TlgDPagHh+eBKOnH2VYvk8qbwsCG/TAJdmTL7f1PROUcSO8qt/KSmShEQ/OKvock8X9tFjtqjCScyOkkkvIKVQ==",
"requires": {
"xtend": "^4.0.1"
}
},
"remark-stringify": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz",
"integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==",
"requires": {
"ccount": "^1.0.0",
"is-alphanumeric": "^1.0.0",
"is-decimal": "^1.0.0",
"is-whitespace-character": "^1.0.0",
"longest-streak": "^2.0.1",
"markdown-escapes": "^1.0.0",
"markdown-table": "^1.1.0",
"mdast-util-compact": "^1.0.0",
"parse-entities": "^1.0.2",
"repeat-string": "^1.5.4",
"state-toggle": "^1.0.0",
"stringify-entities": "^1.0.1",
"unherit": "^1.0.4",
"xtend": "^4.0.1"
}
},
"stringify-entities": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",
"integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==",
"requires": {
"character-entities-html4": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-hexadecimal": "^1.0.0"
}
},
"style-to-object": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.2.3.tgz",
"integrity": "sha512-1d/k4EY2N7jVLOqf2j04dTc37TPOv/hHxZmvpg8Pdh8UYydxeu/C1W1U4vD8alzf5V2Gt7rLsmkr4dxAlDm9ng==",
"requires": {
"inline-style-parser": "0.1.1"
}
},
"unified": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz",
"integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==",
"requires": {
"bail": "^1.0.0",
"extend": "^3.0.0",
"is-plain-obj": "^1.1.0",
"trough": "^1.0.0",
"vfile": "^2.0.0",
"x-is-string": "^0.1.0"
}
},
"unist-builder": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-1.0.4.tgz",
"integrity": "sha512-v6xbUPP7ILrT15fHGrNyHc1Xda8H3xVhP7/HAIotHOhVPjH5dCXA097C3Rry1Q2O+HbOLCao4hfPB+EYEjHgVg==",
"requires": {
"object-assign": "^4.1.0"
}
},
"unist-util-is": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.3.tgz",
"integrity": "sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA=="
},
"unist-util-remove-position": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz",
"integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==",
"requires": {
"unist-util-visit": "^1.1.0"
}
},
"unist-util-stringify-position": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
"integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ=="
},
"unist-util-visit": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz",
"integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==",
"requires": {
"unist-util-visit-parents": "^2.0.0"
}
},
"unist-util-visit-parents": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz",
"integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==",
"requires": {
"unist-util-is": "^3.0.0"
},
"dependencies": {
"unist-util-is": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz",
"integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A=="
}
}
},
"vfile": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz",
"integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==",
"requires": {
"is-buffer": "^1.1.4",
"replace-ext": "1.0.0",
"unist-util-stringify-position": "^1.0.0",
"vfile-message": "^1.0.0"
}
},
"vfile-message": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz",
"integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==",
"requires": {
"unist-util-stringify-position": "^1.1.1"
}
}
}
},
"gatsby-transformer-sharp": {
"version": "2.5.11",
"resolved": "https://registry.npmjs.org/gatsby-transformer-sharp/-/gatsby-transformer-sharp-2.5.11.tgz",
@ -9418,6 +9725,21 @@
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
},
"github-slugger": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz",
"integrity": "sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q==",
"requires": {
"emoji-regex": ">=6.0.0 <=6.1.1"
},
"dependencies": {
"emoji-regex": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz",
"integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4="
}
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@ -9734,6 +10056,17 @@
"resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz",
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="
},
"gray-matter": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
"integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
"requires": {
"js-yaml": "^3.11.0",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
}
},
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
@ -9953,6 +10286,11 @@
"xtend": "^4.0.1"
}
},
"hast-util-is-element": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.4.tgz",
"integrity": "sha512-NFR6ljJRvDcyPP5SbV7MyPBgF47X3BsskLnmw1U34yL+X6YC0MoBx9EyMg8Jtx4FzGH95jw8+c1VPLHaRA0wDQ=="
},
"hast-util-parse-selector": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz",
@ -9973,6 +10311,49 @@
"zwitch": "^1.0.0"
}
},
"hast-util-to-html": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-4.0.1.tgz",
"integrity": "sha512-2emzwyf0xEsc4TBIPmDJmBttIw8R4SXAJiJZoiRR/s47ODYWgOqNoDbf2SJAbMbfNdFWMiCSOrI3OVnX6Qq2Mg==",
"requires": {
"ccount": "^1.0.0",
"comma-separated-tokens": "^1.0.1",
"hast-util-is-element": "^1.0.0",
"hast-util-whitespace": "^1.0.0",
"html-void-elements": "^1.0.0",
"property-information": "^4.0.0",
"space-separated-tokens": "^1.0.0",
"stringify-entities": "^1.0.1",
"unist-util-is": "^2.0.0",
"xtend": "^4.0.1"
},
"dependencies": {
"property-information": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-4.2.0.tgz",
"integrity": "sha512-TlgDPagHh+eBKOnH2VYvk8qbwsCG/TAJdmTL7f1PROUcSO8qt/KSmShEQ/OKvock8X9tFjtqjCScyOkkkvIKVQ==",
"requires": {
"xtend": "^4.0.1"
}
},
"stringify-entities": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",
"integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==",
"requires": {
"character-entities-html4": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-hexadecimal": "^1.0.0"
}
},
"unist-util-is": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.3.tgz",
"integrity": "sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA=="
}
}
},
"hast-util-to-parse5": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-5.1.2.tgz",
@ -9985,6 +10366,11 @@
"zwitch": "^1.0.0"
}
},
"hast-util-whitespace": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz",
"integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A=="
},
"hastscript": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz",
@ -10084,6 +10470,14 @@
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz",
"integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA=="
},
"html-parse-stringify2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
"requires": {
"void-elements": "^2.0.1"
}
},
"html-tag-names": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/html-tag-names/-/html-tag-names-1.1.5.tgz",
@ -10258,6 +10652,14 @@
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
},
"i18next": {
"version": "19.6.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.6.0.tgz",
"integrity": "sha512-t+pA7iN2WtwS1UQc4PFKHDIO4HYZIl2Wo8UC8gqt70Q1qY50FflAF5vV4IbQEqy4DuK3I9wv3BL1PMvkk238WA==",
"requires": {
"@babel/runtime": "^7.10.1"
}
},
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
@ -12480,6 +12882,50 @@
"unist-util-visit": "^2.0.0"
}
},
"mdast-util-to-nlcst": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/mdast-util-to-nlcst/-/mdast-util-to-nlcst-3.2.3.tgz",
"integrity": "sha512-hPIsgEg7zCvdU6/qvjcR6lCmJeRuIEpZGY5xBV+pqzuMOvQajyyF8b6f24f8k3Rw8u40GwkI3aAxUXr3bB2xag==",
"requires": {
"nlcst-to-string": "^2.0.0",
"repeat-string": "^1.5.2",
"unist-util-position": "^3.0.0",
"vfile-location": "^2.0.0"
},
"dependencies": {
"vfile-location": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz",
"integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA=="
}
}
},
"mdast-util-to-string": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz",
"integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A=="
},
"mdast-util-toc": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-5.0.3.tgz",
"integrity": "sha512-A3xzcgC1XFHK0+abFmbINOxjwo7Bi0Nsfp3yTgTy5JHo2q2V6YZ5BVJreDWoK3szcLlSMvHqe8WPbjY50wAkow==",
"requires": {
"@types/mdast": "^3.0.3",
"@types/unist": "^2.0.3",
"extend": "^3.0.2",
"github-slugger": "^1.2.1",
"mdast-util-to-string": "^1.0.5",
"unist-util-is": "^4.0.0",
"unist-util-visit": "^2.0.0"
},
"dependencies": {
"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=="
}
}
},
"mdn-data": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
@ -13026,6 +13472,11 @@
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"nlcst-to-string": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz",
"integrity": "sha512-3x3jwTd6UPG7vi5k4GEzvxJ5rDA7hVUIRNHPblKuMVP9Z3xmlsd9cgLcpAMkc5uPOBna82EeshROFhsPkbnTZg=="
},
"no-case": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz",
@ -13736,6 +14187,17 @@
"xml2js": "^0.4.5"
}
},
"parse-english": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/parse-english/-/parse-english-4.1.3.tgz",
"integrity": "sha512-IQl1v/ik9gw437T8083coohMihae0rozpc7JYC/9h6hi9xKBSxFwh5HWRpzVC2ZhEs2nUlze2aAktpNBJXdJKA==",
"requires": {
"nlcst-to-string": "^2.0.0",
"parse-latin": "^4.0.0",
"unist-util-modify-children": "^1.0.0",
"unist-util-visit-children": "^1.0.0"
}
},
"parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
@ -13765,6 +14227,16 @@
"lines-and-columns": "^1.1.6"
}
},
"parse-latin": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-4.2.1.tgz",
"integrity": "sha512-7T9g6mIsFFpLlo0Zzb2jLWdCt+H9Qtf/hRmMYFi/Mq6Ovi+YKo+AyDFX3OhFfu0vXX5Nid9FKJGKSSzNcTkWiA==",
"requires": {
"nlcst-to-string": "^2.0.0",
"unist-util-modify-children": "^1.0.0",
"unist-util-visit-children": "^1.0.0"
}
},
"parse-passwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
@ -15557,6 +16029,15 @@
}
}
},
"react-i18next": {
"version": "11.7.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.0.tgz",
"integrity": "sha512-8tvVkpuxQlubcszZON+jmoCgiA9gCZ74OAYli9KChPhETtq8pJsANBTe9KRLRLmX3ubumgvidURWr0VvKz1tww==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
}
},
"react-icons": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz",
@ -16061,6 +16542,141 @@
}
}
},
"remark": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz",
"integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==",
"requires": {
"remark-parse": "^6.0.0",
"remark-stringify": "^6.0.0",
"unified": "^7.0.0"
},
"dependencies": {
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
},
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"markdown-table": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz",
"integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q=="
},
"mdast-util-compact": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz",
"integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==",
"requires": {
"unist-util-visit": "^1.1.0"
}
},
"parse-entities": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz",
"integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==",
"requires": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
}
},
"remark-stringify": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz",
"integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==",
"requires": {
"ccount": "^1.0.0",
"is-alphanumeric": "^1.0.0",
"is-decimal": "^1.0.0",
"is-whitespace-character": "^1.0.0",
"longest-streak": "^2.0.1",
"markdown-escapes": "^1.0.0",
"markdown-table": "^1.1.0",
"mdast-util-compact": "^1.0.0",
"parse-entities": "^1.0.2",
"repeat-string": "^1.5.4",
"state-toggle": "^1.0.0",
"stringify-entities": "^1.0.1",
"unherit": "^1.0.4",
"xtend": "^4.0.1"
}
},
"stringify-entities": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",
"integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==",
"requires": {
"character-entities-html4": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-hexadecimal": "^1.0.0"
}
},
"unified": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz",
"integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==",
"requires": {
"@types/unist": "^2.0.0",
"@types/vfile": "^3.0.0",
"bail": "^1.0.0",
"extend": "^3.0.0",
"is-plain-obj": "^1.1.0",
"trough": "^1.0.0",
"vfile": "^3.0.0",
"x-is-string": "^0.1.0"
}
},
"unist-util-stringify-position": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
"integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ=="
},
"unist-util-visit": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz",
"integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==",
"requires": {
"unist-util-visit-parents": "^2.0.0"
}
},
"unist-util-visit-parents": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz",
"integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==",
"requires": {
"unist-util-is": "^3.0.0"
}
},
"vfile": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz",
"integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==",
"requires": {
"is-buffer": "^2.0.0",
"replace-ext": "1.0.0",
"unist-util-stringify-position": "^1.0.0",
"vfile-message": "^1.0.0"
}
},
"vfile-message": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz",
"integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==",
"requires": {
"unist-util-stringify-position": "^1.1.1"
}
}
}
},
"remark-footnotes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-1.0.0.tgz",
@ -16249,6 +16865,14 @@
}
}
},
"remark-retext": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/remark-retext/-/remark-retext-3.1.3.tgz",
"integrity": "sha512-UujXAm28u4lnUvtOZQFYfRIhxX+auKI9PuA2QpQVTT7gYk1OgX6o0OUrSo1KOa6GNrFX+OODOtS5PWIHPxM7qw==",
"requires": {
"mdast-util-to-nlcst": "^3.2.0"
}
},
"remark-squeeze-paragraphs": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz",
@ -16441,6 +17065,15 @@
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
"retext-english": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/retext-english/-/retext-english-3.0.4.tgz",
"integrity": "sha512-yr1PgaBDde+25aJXrnt3p1jvT8FVLVat2Bx8XeAWX13KXo8OT+3nWGU3HWxM4YFJvmfqvJYJZG2d7xxaO774gw==",
"requires": {
"parse-english": "^4.0.0",
"unherit": "^1.0.4"
}
},
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@ -16535,6 +17168,55 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sanitize-html": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.0.tgz",
"integrity": "sha512-U1btucGeYVpg0GoK43jPpe/bDCV4cBOGuxzv5NBd0bOjyZdMKY0n98S/vNlO1wVwre0VCj8H3hbzE7gD2+RjKA==",
"requires": {
"chalk": "^2.4.1",
"htmlparser2": "^4.1.0",
"lodash": "^4.17.15",
"postcss": "^7.0.27",
"srcset": "^2.0.1",
"xtend": "^4.0.1"
},
"dependencies": {
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
},
"domhandler": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz",
"integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==",
"requires": {
"domelementtype": "^2.0.1"
}
},
"domutils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.1.0.tgz",
"integrity": "sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg==",
"requires": {
"dom-serializer": "^0.2.1",
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0"
}
},
"htmlparser2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz",
"integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0",
"domutils": "^2.0.0",
"entities": "^2.0.0"
}
}
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -16559,6 +17241,25 @@
"ajv-keywords": "^3.4.1"
}
},
"section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"requires": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"requires": {
"is-extendable": "^0.1.0"
}
}
}
},
"seek-bzip": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
@ -17466,6 +18167,11 @@
}
}
},
"srcset": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz",
"integrity": "sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ=="
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
@ -17760,6 +18466,11 @@
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
},
"strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI="
},
"strip-comments": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz",
@ -18595,6 +19306,15 @@
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
"integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo="
},
"underscore.string": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz",
"integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==",
"requires": {
"sprintf-js": "^1.0.3",
"util-deprecate": "^1.0.2"
}
},
"unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
@ -18700,6 +19420,14 @@
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz",
"integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A=="
},
"unist-util-modify-children": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-1.1.6.tgz",
"integrity": "sha512-TOA6W9QLil+BrHqIZNR4o6IA5QwGOveMbnQxnWYq+7EFORx9vz/CHrtzF36zWrW61E2UKw7sM1KPtIgeceVwXw==",
"requires": {
"array-iterate": "^1.0.0"
}
},
"unist-util-position": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz",
@ -18728,6 +19456,31 @@
"unist-util-visit": "^2.0.0"
}
},
"unist-util-select": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-1.5.0.tgz",
"integrity": "sha1-qTwr6MD2U4J4A7gTMa3sKqJM2TM=",
"requires": {
"css-selector-parser": "^1.1.0",
"debug": "^2.2.0",
"nth-check": "^1.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"unist-util-stringify-position": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
@ -18753,6 +19506,11 @@
}
}
},
"unist-util-visit-children": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-1.1.4.tgz",
"integrity": "sha512-sA/nXwYRCQVRwZU2/tQWUqJ9JSFM1X3x7JIOsIgSzrFHcfVt6NkzDtKzyxg2cZWkCwGF9CO8x4QNZRJRMK8FeQ=="
},
"unist-util-visit-parents": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz",
@ -19278,6 +20036,11 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",

View File

@ -39,7 +39,9 @@
"gatsby-plugin-sharp": "^2.6.19",
"gatsby-source-filesystem": "^2.3.19",
"gatsby-source-gravatar": "^1.0.0",
"gatsby-transformer-remark": "^2.8.25",
"gatsby-transformer-sharp": "^2.5.11",
"i18next": "^19.6.0",
"lodash": "^4.17.19",
"moment": "^2.27.0",
"nanoevents": "^5.1.8",
@ -47,6 +49,7 @@
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1",
"react-helmet": "^6.1.0",
"react-i18next": "^11.7.0",
"react-icons": "^3.10.0",
"react-markdown": "^4.3.1",
"react-scroll": "^1.8.0",

View File

@ -0,0 +1,38 @@
---
slug: "/blog/acing-video-interviews"
date: "2020-07-15"
title: "Acing Video Interviews"
---
Today, as in-person interviewing has had to cease or slow due to restrictions during the Covid-19 pandemic, thousands of professionals are now needing to learn how to effectively interview in a new way using online platforms, such as Zoom, GoToMeeting and others.
Here are 10 helpful tips for making the best impression you can in your video interview, and demonstrating that youre a great fit for the role.
#### Be conscious of whats in the view
While so many of us are now working remotely and using Zoom or other platforms for our meetings, weve grown more accustomed to seeing people in their home settings, and noticing their home décor, pets, family members, and other aspects of their personal life in the background.
For an interview, its fine to be in your home or living room but try to present whatever people see as neutral and professional as possible. You want to let yourself and your words, conversation and experience speak most powerfully about your qualifications and suitability for the job. And you want to avoid the chance that your interviewer will be distracted by whats behind you, or perhaps have a negative reaction to any personal items (such as a plate of food behind you or a messy room) in your home.
#### Select professional attire
Even though youre conducting the interview from your home, remember you are being judged and assessed for your fit for the role, so dress professionally, just as you would if you were meeting in person.
#### Ready your sound and video equipment
Make sure that you have working Wi-Fi, a strong connection, and a quality headset or microphone so there are no tech issues during your call. Invest in quality equipment for audio and video work.
#### Demonstrate positive body language and behavior
Just as in an in-person interview, you want to demonstrate through your voice and body language that youre interested, engaged, and professional in demeanor and language. Make sure you are not distracted (with your pet, or by loud sounds or interruptions in your home, etc.). If you know there will be significant interruptions or distractions during the scheduled time for the interview, see if you can change it to a time when those interruptions are at minimum.
#### Engage the interviewer with eye contact and connection
Make sure you smile, come across as engaging and interested, and make strong eye contact. Try not to look away during your interview or look down at your notes too frequently. Your eye contact reveals a good deal about how youre feeling and thinking about what the interviewer is sharing with you.
#### As with every interview, prepare, prepare, prepare
Be fully prepared for your interview. Do your research in advance, understand clearly from what the hiring manager has shared in advance what theyre looking for in the role and be ready to talk about why youre potentially very well suited to it. Have in front of you some written sound bites and bullet points that speak to how you can leverage your great talents and abilities and hit the ground running successfully in this job.
#### Finally, remember that youre talented, experienced and have so much value to offer and that the interview is a two-way street
Dont lose sight of the fact that you have a great deal to offer and so much experience and talent to leverage to be of service in important ways. Make sure too that you understand this is a two-way street and you are interviewing the hiring manager about the role and the organization just as much as they are interviewing you. Have your list of questions that you want to make sure you cover so that you will get a strong sense of this role, the work, and if you would truly be a fit, both emotionally and functionally.

View File

@ -0,0 +1,46 @@
---
slug: "/blog/ats-friendly-resumes"
date: "2020-07-14"
title: "ATS-Friendly Resumes"
---
An ATS (Applicant Tracking System) is software used by companies to help them quickly evaluate potential candidates for any given job opening.
ATS software automatically scans and processes each job application a company receives, and ranks them according to their relevant qualifications. It then produces a shortlist of qualified candidates to be reviewed by a hiring manager. If your resume doesnt meet the requirements of a companys ATS, your application will likely be rejected before a hiring manager even gets to look at it.
Applicant tracking systems (ATS) eliminate over 70% of applicants before their resume even reaches a hiring manager. Make sure your application makes the cut by learning how to write an ATS-friendly resume with our expert tips, examples, and ATS resume templates.
#### What is an ATS-compliant Resume?
An ATS-compliant resume is a resume designed specifically to make it easier for ATS software to find the information its looking for.
For example, this could mean using an easy-to-read resume format, or removing objects such as tables or images because theyre difficult for the ATS to parse. Resumes designed to be compliant with ATS software have a much higher chance of getting into the hands of a human hiring manager, which is one step closer to an interview.
#### How to design an ATS-friendly Resume
Here are six tips to help you make a more ATS-friendly resume and ultimately beat the applicant tracking system.
#### 1. Follow a standard resume format
Use a chronological resume to ensure the software can parse your experience section.
#### 2. Correctly label your section
By sticking to common headings, you prevent the bot from placing your qualifications under the wrong categories, or misreading your sections altogether.
#### 3. Include job-related keywords
To help determine whether your qualifications are relevant to the position, ATS software scans your resume for specific job-related resume keywords. To increase your chance of getting into the interview pool, look through the job listing for these words to include on your resume.
#### 4. Use an ATS-friendly resume template
Many job seekers use fancy resume templates to help them stand out from other candidates. However, templates with graphic elements, tables, or unique fonts are difficult for most ATS software to read.
#### 5. Use a common resume font
Most ATS software is programmed to read more common typefaces. Using an unusual or outdated looking font can result in your resume being rendered incorrectly, with large chunks of your information left unreadable.
#### 6. Save your resume as the proper file type
PDFs are the preferred file format for most companies today, and are easily understood by any modern applicant tracking system.

View File

@ -0,0 +1,92 @@
---
slug: "/blog/design-beautiful-resumes"
date: "2020-07-13"
title: "Designing Beautiful Resumes"
---
Follow these 16 pro tips to help your design resume stand out from the crowd.
With designers fighting it out for every job that comes along, it's important that you stand out from the crowd. Whether you're just starting out or a seasoned pro applying for a better position, your design resume needs to be first rate for you to stand a chance of getting an interview.
For your design resume to really shine, you need to think carefully about how it's designed as well as what's written. Here, we'll cover both, as we walk you through the process of creating a stellar designer resume. You'll be landing that dream design job in no time.
#### 1. Avoid word processors
Microsoft Word might be okay if youre applying for an admin position, but if youre after a design job or something creative, its limited and idiosyncratic layout options just won't cut it. Art directors will be paying close attention to the layout of your resume as much as the content, so use InDesign CC or even Illustrator CC to design something special.
Whatever program you use to design your resume in, PDF is the best format to supply it in. This enables you to create good-looking documents that are completely cross-platform.
#### 2. Choose your fonts wisely
Youre a designer, so your resume should follow the latest trends in typography, right? Wrong! The aim of any resume should be legibility, so its generally a wise idea to stick to simple, readable fonts. You don't need to shell out lots of cash to find something suitable either take a look at our list of the best free fonts for designers.
And if you would like to use more than one font, you can also check our perfect font pairings.
#### 3. Consider using colour
For most non-design-related jobs, a resume designed or printed in colour is probably a waste of time. However, for design positions, touches of colour are an acceptable way to add a discreet personal touch. Use colour carefully, however, and don't go over the top. Green type on a yellow page will stand out for all the wrong reasons. See our post on colour theory for more info on this.
#### 4. Be brief
Art directors do not have the time or the inclination to read your entire life story. Your resume should ideally fit onto one side of A4, and if it's any longer than two pages, youre waffling and including too much stuff.
Dont be tempted to mask a lack of experience with verbosity. Clean, well-laid-out resumes will always win over flabby ones remember, the aim is to intrigue and impress. Point the recipient in the direction of an online portfolio to see more.
#### 5. Include your contact info
As a minimum, your resume should include your name and contact details, including your email address, phone number and online portfolio URL. Don't assume that because these are at the bottom of the email you sent, you don't need to include them. Make life easier for your potential employer.
This should be followed by a breakdown of your work experience, then your education. In both cases, this should be most recent first. Work experience should include dates, job title and a brief synopsis of your role. Don't bother including jobs you did years ago that are irrelevant to the job you're applying for. References are generally optional.
#### 6. Don't lie on your resume
We once received a resume from an unnamed individual who claimed to have created quite a stunning website. We would have been extremely impressed were it not for the fact that we had actually designed the site.
Needless to say, that resume went straight in the bin and the sender was rewarded with a strongly worded email. Honesty is always the best policy, as you stand a good chance of being found out if you start 'elaborating' in your resume.
#### 6. Include samples of work
By not including any samples of your work with your resume, youre pretty much guaranteeing that the recipient will not consider you for the post. If you work with motion, stills are perfect, unless youve been specifically asked to include a showreel. On the other hand, don't go overboard with images that's a job for your online graphic design portfolio, which you can provide a link to. Alternatively, you can provide a curated version of your portfolio in PDF format.
#### 7. Keep it simple
Unless youre really confident and sure about what youre doing, keep the typographic flourishes and fanciful designs at bay, ensure the layout is simple and clear and the information is cleanly presented. After all, the last thing you want is the recipient squinting because you thought dark grey text on a black background was a great idea.
#### 8. Show your personality
Simple does not have to mean dull. A resume is a reflection of your disposition and persona, and the recipient will be scanning it, consciously or not, for elements that distinguish your resume from the other hundreds they have to wade through. Make your resume stand out with an idiosyncratic design and personal touches... just don't overdo it.
#### 9. Beware the novelty approach to resumes
Weve had resumes written on scrunched up paper; arriving in the form of a jigsaw; and playing cards. Weve had giant resume posters, inflatable resumes and resumes crafted using delicate and complex paper engineering.
Off-the-wall resumes stick in the mind (you can see some of the best examples in our roundup of creative resumes) but they're a risky proposition. On the one hand you might appear like a creative thinker, on the other it might seem pretentious and excessive. It depends on the recipient.
#### 10. Don't plagiarise
We've all seen this clever resume concept... so don't try to pass it off as your idea
A surprising number of graduates see an inspiring resume design concept and copy it. What can they be thinking? We all have access to the same internet, and if a particularly inventive resume design has caught your eye, there's a strong chance it's been shared virally within the industry and will have caught the eye of your potential employer, too. Your resume should showcase your creativity, not someone else's.
#### 11. Use proper prints, not photocopies
Photocopies are cheap, but sadly they also look cheap, especially second and third generation copies. Type starts to break up, images are contrasty and full of noise, fingerprints and other blemishes begin to show up, and the results can look slightly askew. Fresh laser prints or sharp inkjet prints on the best quality paper available are the minimum standard. For more info, check out our designer's guide to printing.
#### 12. Demonstrate consistency
Real-world design projects are usually centred around a single, consistent theme or concept that runs throughout the logo, branding, literature and so on. Your résumé, portfolio and covering letter need to demonstrate the same consistency. For example, are bulleted lists presented in the same style across each of your pages? Is the colour scheme consistent?
#### 13. Spend time on the covering letter
Most of the time, when you apply for a job, your resume will need to be accompanied by a covering letter. This should look formal and business-like: this isn't the place to showcase your creativity and imagination. The text should complement the CV and it's best to keep it short and to the point (three paragraphs is a good rule of thumb).
Make it obvious you haven't just copied and pasted the same letter you've used to apply for a hundred other jobs. Write it in a way that's personal to the particular job and company you're applying for.
#### 14. Create multiple resumes
If you're applying for multiple jobs, you should create multiple resumes, each targeting a specific role and the kind of experience and skills the prospective employers are looking for. To take an obvious example, if the job specifically mentions InDesign as a requirement then you should make this first on your list of skills, and possibly expand the description of how and where you've used it.
#### 15. Check your spelling!
If you're getting this one wrong, you're in trouble
If you're applying for a job as a designer, does it matter how well you write? The simple answer is yes. Spelling and grammar mistakes will make you appear uneducated, ignorant and/or lazy and none of these represent the image you're trying to convey. So, always double-check your grammar and spelling, and get others to check it too (it's easy to miss your own mistakes)

View File

@ -0,0 +1,38 @@
---
slug: "/blog/jobs-during-covid-19"
date: "2020-07-16"
title: "Jobs During COVID-19"
---
As companies move to remote work to fight the coronavirus pandemic and an increasing number of workers are being laid off or furloughed, you might be wondering if you should continue to send out resumes or just assume that no one is hiring for the foreseeable future. Its true that economists are predicting a recession, but career experts say its best to keep networking and applying, provided you change your approach a bit to acknowledge these are uncertain times.
Be prepared for job openings to be put on hold or disappear, even if theyve been open for a while. That doesnt mean they wont open up again in a few months. Landers admits she herself was getting ready to hire someone but decided to put that on hold for a few weeks.
With all that said, you can still be actively working on your job search. These tips will help you navigate the process during the pandemic and the accompanying economic slowdown.
#### 1. Consider How Urgent You Are Searching
While many industries have and will continue to be hit hard by the COVID-19 pandemic, others are still hiring. If youre unemployed and need a stopgap, consider looking there or wherever else you can find an opportunity that makes sense for you—and pays the rent and puts food on the table—in the meantime.
#### 2. Get Comfortable Networking Online
Events will be cancelled for a while, so youll need to find a new networking strategy. Seek out like-minded professionals online and ask about virtual events.
Look for professional groups to join on Facebook and LinkedIn. Both platforms offer a wide range of options with groups for every profession. For instance, if youre looking for a job in marketing, you could join LinkedIns Global Marketing and Communications Professionals group. Join in the conversation, post and comment, and make yourself visible.
#### 3. Stay In Touch
Maybe you recently had a promising interview and a job offer seemed to be on the horizon, but now the company has moved to remote work and you havent heard from the hiring manager. What should you do? Check in with the hiring manager by email, acknowledging that they might be scrambling to help their employees get used to the new setup.
#### 4. Gather Intel
The COVID-19 crisis can provide a unique glimpse into company culture. Take note of how leadership deals with this emergency and treats its employees by following the company on social media and watching for any media coverage.
You can mention what you read and listened to and use your specific knowledge to drive home how you could help the company achieve its goals if hired.
#### 5. Use the Time to Reflect
Job seekers often jump at the first available opportunity or go into their search without fully considering what they want to do next. Take advantage of the slowing job market by getting clarity about where you want to work and the type of role and title you're seeking.
#### 6. Boost Your Skills
Now is the perfect time to work on bolstering your qualifications, Moser says. Analyze job descriptions by listing each required skill and experience. Then consider whether you have that exact skill, if you have the skill but havent used it in a few years, or if youre lacking the skill entirely. Use that information to determine what you need to brush up on to make yourself an even better candidate when the job market picks up again.

47
src/components/Blog.js Normal file
View File

@ -0,0 +1,47 @@
/* eslint-disable react/no-danger */
import { graphql } from 'gatsby';
import React from 'react';
import { Helmet } from 'react-helmet';
import styles from './Blog.module.css';
import Wrapper from './shared/Wrapper';
import Hero from './landing/Hero';
export default function Template({ data }) {
const { markdownRemark } = data;
const { frontmatter, html } = markdownRemark;
return (
<Wrapper>
<div className="my-24 container">
<Hero />
<Helmet>
<title>{frontmatter.title} | Reactive Resume</title>
<link
rel="canonical"
href={`https://rxresu.me/blog/${frontmatter.slug}`}
/>
</Helmet>
<h1 className="mt-16 text-3xl">{frontmatter.title}</h1>
<h2>{frontmatter.date}</h2>
<div
className={styles.container}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
</Wrapper>
);
}
export const pageQuery = graphql`
query($slug: String!) {
markdownRemark(frontmatter: { slug: { eq: $slug } }) {
html
frontmatter {
date(formatString: "MMMM DD, YYYY")
slug
title
}
}
}
`;

View File

@ -0,0 +1,11 @@
.container {
@apply leading-loose my-8;
}
.container p {
@apply my-4;
}
.container h4 {
@apply font-medium text-xl;
}

View File

@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { useSelector } from '../../../contexts/ResumeContext';
import Castform from '../../../templates/Castform';
import Celebi from '../../../templates/Celebi';
@ -11,17 +12,20 @@ import styles from './Artboard.module.css';
const Artboard = () => {
const state = useSelector();
const { t } = useTranslation();
const { id, name, metadata } = state;
const { template } = metadata;
return (
<div>
<>
<Helmet>
<title>{name} | Reactive Resume</title>
<title>
{name} | {t('shared.appName')}
</title>
<link rel="canonical" href={`https://rxresu.me/app/builder/${id}`} />
</Helmet>
<div id="artboard" className={styles.container}>
<div className={styles.container}>
{template === 'onyx' && <Onyx data={state} />}
{template === 'pikachu' && <Pikachu data={state} />}
{template === 'gengar' && <Gengar data={state} />}
@ -29,7 +33,7 @@ const Artboard = () => {
{template === 'glalie' && <Glalie data={state} />}
{template === 'celebi' && <Celebi data={state} />}
</div>
</div>
</>
);
};

View File

@ -3,8 +3,10 @@
width: 210mm;
height: 297mm;
zoom: 0.8;
overflow: scroll;
-moz-transform: scale(0.8);
transform-origin: 50% 18%;
overflow-y: scroll;
box-shadow: var(--shadow);
@apply bg-white rounded;
}
}
}

View File

@ -1,39 +1,43 @@
import { Link } from 'gatsby';
import React, { memo } from 'react';
import { Tooltip } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import sections from '../../../data/leftSections';
import Avatar from '../../shared/Avatar';
import Logo from '../../shared/Logo';
import SectionIcon from '../../shared/SectionIcon';
import styles from './LeftNavbar.module.css';
const LeftNavbar = () => (
<div className={styles.container}>
<Tooltip title="Go Back to Dashboard" placement="right">
<div>
<Link to="/app/dashboard">
<Logo size="40px" />
</Link>
const LeftNavbar = () => {
const { t } = useTranslation();
return (
<div className={styles.container}>
<Tooltip title={t('builder.tooltips.backToDashboard')} placement="right">
<div>
<Link to="/app/dashboard">
<Logo size="40px" />
</Link>
</div>
</Tooltip>
<hr className="my-6" />
<div className="grid grid-cols-1 gap-4 text-primary-500">
{sections.map((x) => (
<SectionIcon
key={x.id}
section={x}
containerId="LeftSidebar"
tooltipPlacement="right"
/>
))}
</div>
</Tooltip>
<hr className="my-6" />
<hr className="mt-auto my-6" />
<div className="grid grid-cols-1 gap-4 text-primary-500">
{sections.map((x) => (
<SectionIcon
key={x.id}
section={x}
containerId="LeftSidebar"
tooltipPlacement="right"
/>
))}
<Avatar />
</div>
<hr className="mt-auto my-6" />
<Avatar />
</div>
);
);
};
export default memo(LeftNavbar);

View File

@ -1,5 +1,6 @@
import React, { Fragment, memo } from 'react';
import { Element } from 'react-scroll';
import { useTranslation } from 'react-i18next';
import sections from '../../../data/leftSections';
import LeftNavbar from './LeftNavbar';
import styles from './LeftSidebar.module.css';
@ -48,18 +49,24 @@ const getComponent = (id) => {
};
const LeftSidebar = () => {
const { t } = useTranslation();
return (
<div className="flex">
<LeftNavbar />
<div id="LeftSidebar" className={styles.container}>
{sections.map(({ id, name, event }) => {
{sections.map(({ id, event }) => {
const Component = getComponent(id);
return (
<Fragment key={id}>
<Element name={id}>
<Component id={id} name={name} event={event} />
<Component
id={id}
name={t(`builder.sections.${id}`)}
event={event}
/>
</Element>
<hr />
</Fragment>

View File

@ -1,13 +1,20 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import Heading from '../../../shared/Heading';
import Input from '../../../shared/Input';
const Objective = () => {
const Objective = ({ name }) => {
const { t } = useTranslation();
return (
<section>
<Heading>Objective</Heading>
<Heading>{name}</Heading>
<Input type="textarea" label="Objective" path="objective.body" />
<Input
type="textarea"
label={t('shared.forms.summary')}
path="objective.body"
/>
</section>
);
};

View File

@ -1,45 +1,80 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import Heading from '../../../shared/Heading';
import Input from '../../../shared/Input';
import PhotoUpload from '../../../shared/PhotoUpload';
const Profile = () => {
const Profile = ({ name }) => {
const { t } = useTranslation();
return (
<section>
<Heading>Profile</Heading>
<Heading>{name}</Heading>
<PhotoUpload />
<div className="grid grid-cols-2 gap-6">
<Input name="firstName" label="First Name" path="profile.firstName" />
<Input name="lastName" label="Last Name" path="profile.lastName" />
<Input
name="firstName"
label={t('builder.profile.firstName')}
path="profile.firstName"
/>
<Input
name="lastName"
label={t('builder.profile.lastName')}
path="profile.lastName"
/>
</div>
<Input name="subtitle" label="Subtitle" path="profile.subtitle" />
<Input
name="subtitle"
label={t('shared.forms.subtitle')}
path="profile.subtitle"
/>
<hr />
<Input
name="addressLine1"
label="Address Line 1"
label={t('builder.profile.address.line1')}
path="profile.address.line1"
/>
<Input
name="addressLine2"
label="Address Line 2"
label={t('builder.profile.address.line2')}
path="profile.address.line2"
/>
<div className="grid grid-cols-2 gap-6">
<Input name="city" label="City" path="profile.address.city" />
<Input name="pincode" label="Pincode" path="profile.address.pincode" />
<Input
name="city"
label={t('builder.profile.address.city')}
path="profile.address.city"
/>
<Input
name="pincode"
label={t('builder.profile.address.pincode')}
path="profile.address.pincode"
/>
</div>
<hr />
<Input name="phone" label="Phone Number" path="profile.phone" />
<Input name="website" label="Website" path="profile.website" />
<Input name="email" label="Email Address" path="profile.email" />
<Input
name="phone"
label={t('shared.forms.phone')}
path="profile.phone"
/>
<Input
name="website"
label={t('shared.forms.website')}
path="profile.website"
/>
<Input
name="email"
label={t('shared.forms.email')}
path="profile.email"
/>
</section>
);
};

View File

@ -1,7 +1,12 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
const EmptyList = () => (
<div className="py-6 opacity-75 text-center">This list is empty.</div>
);
const EmptyList = () => {
const { t } = useTranslation();
return (
<div className="py-6 opacity-75 text-center">{t('builder.emptyList')}</div>
);
};
export default memo(EmptyList);

View File

@ -1,5 +1,6 @@
import { Menu, MenuItem } from '@material-ui/core';
import React, { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io';
import { MdMoreVert } from 'react-icons/md';
import { useDispatch } from '../../../contexts/ResumeContext';
@ -15,8 +16,9 @@ const ListItem = ({
isLast,
onEdit,
}) => {
const [anchorEl, setAnchorEl] = useState(null);
const dispatch = useDispatch();
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => setAnchorEl(event.currentTarget);
@ -98,9 +100,11 @@ const ListItem = ({
<IoIosArrowDown size="18px" />
</MenuItem>
</div>
<MenuItem onClick={handleEdit}>Edit</MenuItem>
<MenuItem onClick={handleEdit}>{t('shared.buttons.edit')}</MenuItem>
<MenuItem onClick={handleDelete}>
<span className="text-red-600 font-medium">Delete</span>
<span className="text-red-600 font-medium">
{t('shared.buttons.delete')}
</span>
</MenuItem>
</Menu>
</div>

View File

@ -1,15 +1,16 @@
import React, { Fragment, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Element } from 'react-scroll';
import sections from '../../../data/rightSections';
import RightNavbar from './RightNavbar';
import styles from './RightSidebar.module.css';
import About from './sections/About';
import Actions from './sections/Actions';
import Colors from './sections/Colors';
import Fonts from './sections/Fonts';
import Layout from './sections/Layout';
import Templates from './sections/Templates';
import Actions from './sections/Actions';
import Settings from './sections/Settings';
import About from './sections/About';
import Templates from './sections/Templates';
const getComponent = (id) => {
switch (id) {
@ -33,16 +34,22 @@ const getComponent = (id) => {
};
const RightSidebar = () => {
const { t } = useTranslation();
return (
<div className="flex">
<div id="RightSidebar" className={styles.container}>
{sections.map(({ id, name, event }) => {
{sections.map(({ id, event }) => {
const Component = getComponent(id);
return (
<Fragment key={id}>
<Element name={id}>
<Component id={id} name={name} event={event} />
<Component
id={id}
name={t(`builder.sections.${id}`)}
event={event}
/>
</Element>
<hr />
</Fragment>

View File

@ -1,31 +1,25 @@
import React, { memo } from 'react';
import { FaCoffee, FaBug } from 'react-icons/fa';
import { FaCoffee, FaBug, FaExternalLinkAlt } from 'react-icons/fa';
import { MdCode } from 'react-icons/md';
import { Trans, useTranslation } from 'react-i18next';
import Button from '../../../shared/Button';
import Heading from '../../../shared/Heading';
import styles from './About.module.css';
const About = () => {
const About = ({ name }) => {
const { t } = useTranslation();
return (
<section>
<Heading>About</Heading>
<Heading>{name}</Heading>
<div className={styles.container}>
<h5>Donate to Reactive Resume</h5>
<h5>{t('builder.about.donate.heading')}</h5>
<p className="leading-loose">
As you know, every nook and cranny of this app is free and
open-source, but servers don&apos;t pay for themselves.
</p>
<p className="leading-loose">
I try to do what I can, but if you found the app helpful, or
you&apos;re in a better position than the others who depend on this
project for their first job, please consider donating{' '}
<span className="font-semibold">
as little as $5 to help keep the project alive
</span>{' '}
:)
<Trans t={t} i18nKey="builder.about.donate.text">
A<span className="font-bold">B</span>C
</Trans>
</p>
<div className="mt-4 flex">
@ -34,19 +28,15 @@ const About = () => {
rel="noreferrer"
target="_blank"
>
<Button icon={FaCoffee}>Buy me a coffee!</Button>
<Button icon={FaCoffee}>{t('builder.about.donate.button')}</Button>
</a>
</div>
</div>
<div className={styles.container}>
<h5>Bug? Feature Request?</h5>
<h5>{t('builder.about.bugFeature.heading')}</h5>
<p className="leading-loose">
Something halting your progress from making a resume? Found a pesky
bug that just won&apos;t quit? Talk about it on the GitHub Issues
section, or send me and email using the actions below.
</p>
<p className="leading-loose">{t('builder.about.bugFeature.text')}</p>
<div className="mt-4 flex">
<a
@ -54,19 +44,27 @@ const About = () => {
rel="noreferrer"
target="_blank"
>
<Button icon={FaBug}>Raise an Issue</Button>
<Button icon={FaBug}>{t('builder.about.bugFeature.button')}</Button>
</a>
</div>
</div>
<div className={styles.container}>
<h5>Source Code</h5>
<h5>{t('builder.about.appreciate.heading')}</h5>
<p className="leading-loose">
Want to run the project from its source? Are you a developer willing
to contribute to the open-source development of this project? Click
the button below.
</p>
<p className="leading-loose">{t('builder.about.appreciate.text')}</p>
<div className="mt-4 flex">
<a href="https://amruthpillai.com" rel="noreferrer" target="_blank">
<Button icon={FaExternalLinkAlt}>amruthpillai.com</Button>
</a>
</div>
</div>
<div className={styles.container}>
<h5>{t('builder.about.sourceCode.heading')}</h5>
<p className="leading-loose">{t('builder.about.sourceCode.text')}</p>
<div className="mt-4 flex">
<a
@ -74,38 +72,20 @@ const About = () => {
rel="noreferrer"
target="_blank"
>
<Button icon={MdCode}>GitHub Repo</Button>
</a>
</div>
</div>
<div className={styles.container}>
<h5>License Information</h5>
<p className="leading-loose">
The project is governed under the MIT License, which you can read more
about below. Basically, you are allowed to use the project anywhere
provided you give credits to the original author.
</p>
<div className="mt-4 flex">
<a
href="https://github.com/AmruthPillai/Reactive-Resume/blob/master/LICENSE"
rel="noreferrer"
target="_blank"
>
<Button icon={MdCode}>MIT License</Button>
<Button icon={MdCode}>
{t('builder.about.sourceCode.button')}
</Button>
</a>
</div>
</div>
<div className="my-4 text-center opacity-50 text-sm">
<p>
Made with Love by{' '}
<Trans t={t} i18nKey="builder.about.footer">
A
<a href="https://amruthpillai.com" rel="noreferrer" target="_blank">
Amruth Pillai
B
</a>
</p>
</Trans>
</div>
</section>
);

View File

@ -1,5 +1,6 @@
import React, { memo, useContext, useState } from 'react';
import { FaFileExport, FaFileImport } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import ModalContext from '../../../../contexts/ModalContext';
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
import Button from '../../../shared/Button';
@ -7,9 +8,15 @@ import Heading from '../../../shared/Heading';
import Input from '../../../shared/Input';
import styles from './Actions.module.css';
const Actions = () => {
const [loadDemoText, setLoadDemoText] = useState('Load Demo Data');
const [resetText, setResetText] = useState('Reset Everything');
const Actions = ({ name }) => {
const { t } = useTranslation();
const [loadDemoText, setLoadDemoText] = useState(
t('builder.actions.loadDemoData.button'),
);
const [resetText, setResetText] = useState(
t('builder.actions.resetEverything.button'),
);
const state = useSelector();
const dispatch = useDispatch();
@ -31,66 +38,57 @@ const Actions = () => {
};
const handleLoadDemo = () => {
if (loadDemoText === 'Load Demo Data') {
setLoadDemoText('Are you sure?');
if (loadDemoText === t('builder.actions.loadDemoData.button')) {
setLoadDemoText(t('shared.buttons.confirmation'));
return;
}
dispatch({ type: 'load_demo_data' });
setLoadDemoText('Load Demo Data');
setLoadDemoText(t('builder.actions.loadDemoData.button'));
};
const handleReset = () => {
if (resetText === 'Reset Everything') {
setResetText('Are you sure?');
if (resetText === t('builder.actions.resetEverything.button')) {
setResetText(t('shared.buttons.confirmation'));
return;
}
setResetText('Reset Everything');
setResetText(t('builder.actions.resetEverything.button'));
dispatch({ type: 'reset_data' });
};
return (
<section>
<Heading>Actions</Heading>
<Heading>{name}</Heading>
<div className={styles.container}>
<h5>Import Your Resume</h5>
<h5>{t('builder.actions.import.heading')}</h5>
<p className="leading-loose">
You can import your information from various sources like JSON Resume
or your LinkedIn to autofill most of the data for your resume.
</p>
<p className="leading-loose">{t('builder.actions.import.text')}</p>
<div className="mt-4 flex">
<Button icon={FaFileImport} onClick={handleImport}>
Import
{t('builder.actions.import.button')}
</Button>
</div>
</div>
<div className={styles.container}>
<h5>Export Your Resume</h5>
<h5>{t('builder.actions.export.heading')}</h5>
<p className="leading-loose">
Export your resume as a PDF to share with recruiters or a JSON that
you will be able to import back onto this app on another computer.
</p>
<p className="leading-loose">{t('builder.actions.export.text')}</p>
<div className="mt-4 flex">
<Button icon={FaFileExport} onClick={handleExport}>
Export
{t('builder.actions.export.button')}
</Button>
</div>
</div>
<div className={styles.container}>
<h5>Share Your Resume</h5>
<h5>{t('builder.actions.share.heading')}</h5>
<p className="leading-loose">
The link below will be accessible publicly if you choose to share it,
and viewers would see the latest version of your resume at any time.
</p>
<p className="leading-loose">{t('builder.actions.export.text')}</p>
<div>
<Input
@ -102,11 +100,10 @@ const Actions = () => {
</div>
<div className={styles.container}>
<h5>Load Demo Data</h5>
<h5>{t('builder.actions.loadDemoData.button')}</h5>
<p className="leading-loose">
Unclear on what to do with a fresh blank page? Load some demo data to
see how a resume should look and you can start editing from there.
{t('builder.actions.loadDemoData.text')}
</p>
<div className="mt-4 flex">
@ -115,11 +112,10 @@ const Actions = () => {
</div>
<div className={styles.container}>
<h5>Reset Everything</h5>
<h5>{t('builder.actions.resetEverything.button')}</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.
{t('builder.actions.resetEverything.text')}
</p>
<div className="mt-4 flex">

View File

@ -1,5 +1,6 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from '../../../../contexts/ResumeContext';
import colorOptions from '../../../../data/colorOptions';
import { handleKeyUp } from '../../../../utils';
@ -7,8 +8,9 @@ import Heading from '../../../shared/Heading';
import Input from '../../../shared/Input';
import styles from './Colors.module.css';
const Colors = () => {
const Colors = ({ name }) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const handleClick = (value) => {
dispatch({
@ -22,7 +24,7 @@ const Colors = () => {
return (
<section>
<Heading>Colors</Heading>
<Heading>{name}</Heading>
<div className="mb-6 grid grid-cols-8 col-gap-2 row-gap-6">
{colorOptions.map((color) => (
@ -41,7 +43,7 @@ const Colors = () => {
<Input
type="color"
name="primary"
label="Primary Color"
label={t('builder.colors.primary')}
placeholder="#FF4081"
path="metadata.colors.primary"
/>
@ -49,7 +51,7 @@ const Colors = () => {
<Input
type="color"
name="text"
label="Text Color"
label={t('builder.colors.text')}
placeholder="#444444"
path="metadata.colors.text"
/>
@ -57,7 +59,7 @@ const Colors = () => {
<Input
type="color"
name="background"
label="Background Color"
label={t('builder.colors.background')}
placeholder="#FFFFFF"
path="metadata.colors.background"
/>

View File

@ -6,7 +6,7 @@ import { handleKeyUp } from '../../../../utils';
import Heading from '../../../shared/Heading';
import styles from './Fonts.module.css';
const Fonts = () => {
const Fonts = ({ name }) => {
const dispatch = useDispatch();
const font = useSelector('metadata.font');
@ -22,7 +22,7 @@ const Fonts = () => {
return (
<section>
<Heading>Fonts</Heading>
<Heading>{name}</Heading>
<div className="grid grid-cols-2 gap-8">
{fontOptions.map((x) => (

View File

@ -1,13 +1,17 @@
import React, { memo, useState } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
import { move, reorder } from '../../../../utils';
import Button from '../../../shared/Button';
import Heading from '../../../shared/Heading';
import styles from './Layout.module.css';
const Layout = () => {
const [resetLayoutText, setResetLayoutText] = useState('Reset Layout');
const Layout = ({ name }) => {
const { t } = useTranslation();
const [resetLayoutText, setResetLayoutText] = useState(
t('builder.layout.reset'),
);
const template = useSelector('metadata.template');
const blocks = useSelector(`metadata.layout.${template}`, [[]]);
@ -49,22 +53,21 @@ const Layout = () => {
};
const handleResetLayout = () => {
if (resetLayoutText === 'Reset Layout') {
setResetLayoutText('Are you sure?');
if (resetLayoutText === t('builder.layout.reset')) {
setResetLayoutText(t('shared.buttons.confirmation'));
return;
}
dispatch({ type: 'reset_layout' });
setResetLayoutText('Reset Layout');
setResetLayoutText(t('builder.layout.reset'));
};
return (
<section>
<Heading>Layout</Heading>
<Heading>{name}</Heading>
<p className="leading-loose">
This template supports {blocks.length} blocks. You can re-order or move
sections by dragging/dropping the section names across lists.
{t('builder.layout.text', { count: blocks.length })}
</p>
<div className={`grid gap-8 grid-cols-${blocks.length}`}>
@ -79,7 +82,7 @@ const Layout = () => {
>
<div className="grid gap-3">
<span className="uppercase font-semibold text-xs">
Block {ind + 1}
{t('builder.layout.block')} {ind + 1}
</span>
{el.map((item, index) => (
<Draggable key={item} index={index} draggableId={item}>
@ -90,7 +93,7 @@ const Layout = () => {
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{item}
{t(`builder.sections.${item}`)}
</div>
)}
</Draggable>

View File

@ -3,5 +3,5 @@
}
.draggable {
@apply px-4 py-2 capitalize font-medium rounded bg-primary-200;
@apply px-4 py-2 font-medium rounded bg-primary-200;
}

View File

@ -1,30 +1,42 @@
import React, { memo, useContext, useState } from 'react';
import { FaAngleDown } from 'react-icons/fa';
import { useTranslation, Trans } from 'react-i18next';
import UserContext from '../../../../contexts/UserContext';
import Button from '../../../shared/Button';
import Heading from '../../../shared/Heading';
import styles from './Settings.module.css';
import Input from '../../../shared/Input';
import ThemeContext from '../../../../contexts/ThemeContext';
import SettingsContext from '../../../../contexts/SettingsContext';
import themeConfig from '../../../../data/themeConfig';
import languageConfig from '../../../../data/languageConfig';
import { languages } from '../../../../i18n';
import { useDispatch } from '../../../../contexts/ResumeContext';
const Settings = () => {
const [deleteText, setDeleteText] = useState('Delete Account');
const Settings = ({ name }) => {
const { t } = useTranslation();
const [deleteText, setDeleteText] = useState(
t('builder.settings.dangerZone.button'),
);
const dispatch = useDispatch();
const { deleteAccount } = useContext(UserContext);
const { theme, setTheme } = useContext(ThemeContext);
const { theme, setTheme, language, setLanguage } = useContext(
SettingsContext,
);
const handleChangeTheme = (e) => {
setTheme(e.target.value);
};
const handleChangeLanguage = (e) => {
console.log(e.target.value);
const lang = e.target.value;
setLanguage(lang);
dispatch({ type: 'change_language', payload: lang });
};
const handleDeleteAccount = () => {
if (deleteText === 'Delete Account') {
setDeleteText('Are you sure?');
if (deleteText === t('builder.settings.dangerZone.button')) {
setDeleteText(t('shared.buttons.confirmation'));
return;
}
@ -36,10 +48,10 @@ const Settings = () => {
return (
<section>
<Heading>Settings</Heading>
<Heading>{name}</Heading>
<Input
label="Theme"
label={t('builder.settings.theme')}
type="dropdown"
options={Object.keys(themeConfig)}
value={theme}
@ -47,11 +59,11 @@ const Settings = () => {
/>
<label>
<span>Language</span>
<span>{t('builder.settings.language')}</span>
<div className="relative grid items-center">
<select onChange={handleChangeLanguage}>
{languageConfig.map((x) => (
<option key={x.key} value={x.key}>
<select onChange={handleChangeLanguage} value={language}>
{languages.map((x) => (
<option key={x.code} value={x.code}>
{x.name}
</option>
))}
@ -65,26 +77,23 @@ const Settings = () => {
</label>
<p className="text-sm leading-loose">
If you would like to contribute by providing translations to Reactive
Resume in your language,{' '}
<a
href="https://github.com/AmruthPillai/Reactive-Resume/blob/master/TRANSLATING.md"
rel="noreferrer"
target="_blank"
>
please visit this link
</a>
.
<Trans t={t} i18nKey="builder.settings.translate">
A
<a
href="https://github.com/AmruthPillai/Reactive-Resume/blob/master/TRANSLATING.md"
rel="noreferrer"
target="_blank"
>
B
</a>
C
</Trans>
</p>
<div className={styles.container}>
<h5>Danger Zone</h5>
<h5>{t('builder.settings.dangerZone.heading')}</h5>
<p className="leading-loose">
If you would like to delete your account and erase all your resumes,
its just one button away. Please be weary as this is an irreversible
process.
</p>
<p className="leading-loose">{t('builder.settings.dangerZone.text')}</p>
<div className="mt-4 flex">
<Button isDelete onClick={handleDeleteAccount}>

View File

@ -8,7 +8,7 @@ import { handleKeyUp } from '../../../../utils';
import Heading from '../../../shared/Heading';
import styles from './Templates.module.css';
const Templates = () => {
const Templates = ({ name }) => {
const dispatch = useDispatch();
const template = useSelector('metadata.template');
@ -71,7 +71,7 @@ const Templates = () => {
return (
<section>
<Heading>Templates</Heading>
<Heading>{name}</Heading>
<div className="grid grid-cols-2 gap-8">
{templateOptions.map((x) => (

View File

@ -1,10 +1,12 @@
import React, { memo, useContext } from 'react';
import { MdAdd } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import ModalContext from '../../contexts/ModalContext';
import { handleKeyUp } from '../../utils';
import styles from './CreateResume.module.css';
const CreateResume = () => {
const { t } = useTranslation();
const { emitter, events } = useContext(ModalContext);
const handleClick = () => emitter.emit(events.CREATE_RESUME_MODAL);
@ -24,7 +26,7 @@ const CreateResume = () => {
<MdAdd size="48" />
</div>
<div className={styles.meta}>
<p>Create Resume</p>
<p>{t('dashboard.createResume')}</p>
</div>
</div>
);

View File

@ -4,11 +4,13 @@ import moment from 'moment';
import React, { useContext, useState } from 'react';
import { MdMoreHoriz, MdOpenInNew } from 'react-icons/md';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import DatabaseContext from '../../contexts/DatabaseContext';
import ModalContext from '../../contexts/ModalContext';
import styles from './ResumePreview.module.css';
const ResumePreview = ({ resume }) => {
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const { emitter, events } = useContext(ModalContext);
const { duplicateResume, deleteResume } = useContext(DatabaseContext);
@ -31,7 +33,7 @@ const ResumePreview = ({ resume }) => {
const handleDelete = () => {
deleteResume(resume.id);
toast(`${resume.name} was deleted successfully`);
toast(t('dashboard.toasts.deleted', { name: resume.name }));
setAnchorEl(null);
};
@ -42,10 +44,7 @@ const ResumePreview = ({ resume }) => {
return (
<div className={styles.resume}>
<div className={styles.backdrop}>
<img
src="https://source.unsplash.com/random/210x297"
alt="Resume Preview"
/>
<img src={resume.preview} alt={resume.name} />
</div>
<div className={styles.page}>
<MdOpenInNew
@ -67,17 +66,27 @@ const ResumePreview = ({ resume }) => {
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
<MenuItem onClick={handleRename}>Rename</MenuItem>
<MenuItem onClick={handleDuplicate}>
{t('dashboard.buttons.duplicate')}
</MenuItem>
<MenuItem onClick={handleRename}>
{t('dashboard.buttons.rename')}
</MenuItem>
<MenuItem onClick={handleDelete}>
<span className="text-red-600 font-medium">Delete</span>
<span className="text-red-600 font-medium">
{t('shared.buttons.delete')}
</span>
</MenuItem>
</Menu>
</div>
<div className={styles.meta}>
<span>{resume.name}</span>
{resume.updatedAt && (
<span>Last updated {moment(resume.updatedAt).fromNow()}</span>
<span>
{t('dashboard.lastUpdated', {
timestamp: moment(resume.updatedAt).fromNow(),
})}
</span>
)}
</div>
</div>

View File

@ -1,13 +1,15 @@
import { navigate } from 'gatsby';
import TypeIt from 'typeit-react';
import React, { memo, useContext } from 'react';
import { FaGithub } from 'react-icons/fa';
import TypeIt from 'typeit-react';
import { Link } from '@reach/router';
import { useTranslation } from 'react-i18next';
import ModalContext from '../../contexts/ModalContext';
import UserContext from '../../contexts/UserContext';
import Button from '../shared/Button';
import Logo from '../shared/Logo';
const Hero = () => {
const { t } = useTranslation();
const { emitter, events } = useContext(ModalContext);
const { user, loading } = useContext(UserContext);
@ -17,10 +19,11 @@ const Hero = () => {
return (
<div className="flex items-center">
<Logo className="shadow-lg" size="256px" />
<Link to="/">
<Logo className="shadow-lg" size="256px" />
</Link>
<div className="ml-12">
<h1 className="sr-only">Reactive Resume</h1>
<div className="text-5xl font-bold">
<TypeIt
getBeforeInit={(instance) => {
@ -36,29 +39,19 @@ const Hero = () => {
/>
</div>
<h2 className="mt-1 text-3xl text-primary-500">
A free and open-source resume builder.
{t('shared.shortDescription')}
</h2>
<div className="mt-12 flex">
{user ? (
<Button onClick={handleGotoApp} isLoading={loading}>
Go to App
{t('landing.hero.goToApp')}
</Button>
) : (
<Button onClick={handleLogin} isLoading={loading}>
Login
{t('shared.buttons.login')}
</Button>
)}
<a
href="https://github.com/AmruthPillai/Reactive-Resume"
rel="noreferrer"
target="_blank"
>
<Button outline icon={FaGithub} className="ml-8">
Source Code
</Button>
</a>
</div>
</div>
</div>

View File

@ -2,11 +2,13 @@ import cx from 'classnames';
import { toUrl } from 'gatsby-source-gravatar';
import React, { memo, useContext, useMemo, useState } from 'react';
import { Menu, MenuItem } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import UserContext from '../../contexts/UserContext';
import styles from './Avatar.module.css';
import { handleKeyUp } from '../../utils';
const Avatar = ({ className }) => {
const { t } = useTranslation();
const { user, logout } = useContext(UserContext);
const [anchorEl, setAnchorEl] = useState(null);
@ -43,7 +45,7 @@ const Avatar = ({ className }) => {
onClose={handleClose}
open={Boolean(anchorEl)}
>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
<MenuItem onClick={handleLogout}>{t('shared.buttons.logout')}</MenuItem>
</Menu>
</div>
);

View File

@ -1,5 +1,6 @@
import cx from 'classnames';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { handleKeyUp } from '../../utils';
import styles from './Button.module.css';
@ -12,6 +13,7 @@ const Button = ({
isLoading,
isDelete,
}) => {
const { t } = useTranslation();
const Icon = icon;
return (
@ -24,7 +26,7 @@ const Button = ({
})}
>
{icon && <Icon size="14" className="mr-3" />}
{isLoading ? 'Loading...' : children}
{isLoading ? t('shared.buttons.loading') : children}
</button>
);
};

View File

@ -1,7 +1,7 @@
import React, { memo } from 'react';
const Heading = ({ children }) => {
return <h2 className="text-4xl">{children}</h2>;
return <h2 className="text-4xl focus:outline-none">{children}</h2>;
};
export default memo(Heading);

View File

@ -1,6 +1,7 @@
import cx from 'classnames';
import { isFunction } from 'lodash';
import React, { memo, useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { FaAngleDown } from 'react-icons/fa';
import { MdClose, MdOpenInNew } from 'react-icons/md';
import { v4 as uuidv4 } from 'uuid';
@ -24,6 +25,7 @@ const Input = ({
placeholder,
type = 'text',
}) => {
const { t } = useTranslation();
const [uuid, setUuid] = useState(null);
const stateValue = useSelector(path, '');
const dispatch = useDispatch();
@ -51,7 +53,9 @@ const Input = ({
<span>
{label}{' '}
{isRequired && (
<span className="opacity-75 font-normal lowercase">(Required)</span>
<span className="opacity-75 font-normal lowercase">
({t('shared.forms.required')})
</span>
)}
</span>
@ -92,15 +96,17 @@ const Input = ({
/>
<p className="mt-2 text-sm opacity-75">
This text block supports{' '}
<a
href="https://www.markdownguide.org/basic-syntax/"
className="text-blue-600"
target="blank"
>
markdown
</a>
.
<Trans t={t} i18nKey="shared.forms.markdown">
A
<a
href="https://www.markdownguide.org/basic-syntax/"
className="text-blue-600"
target="blank"
>
B
</a>
C
</Trans>
</p>
</div>
)}

View File

@ -1,6 +1,7 @@
import { Tooltip } from '@material-ui/core';
import React, { memo, useContext, useRef } from 'react';
import { MdFileUpload } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import StorageContext from '../../contexts/StorageContext';
import { handleKeyUp } from '../../utils';
import Input from './Input';
@ -8,6 +9,7 @@ import styles from './PhotoUpload.module.css';
const PhotoUpload = () => {
const fileInputRef = useRef(null);
const { t } = useTranslation();
const { uploadPhotograph } = useContext(StorageContext);
const handleIconClick = () => {
@ -21,7 +23,10 @@ const PhotoUpload = () => {
return (
<div className="flex items-center">
<Tooltip title="Upload Photograph" placement="right-start">
<Tooltip
title={t('builder.tooltips.uploadPhotograph')}
placement="right-start"
>
<div
role="button"
tabIndex="0"
@ -42,7 +47,7 @@ const PhotoUpload = () => {
<Input
name="photograph"
label="Photograph"
label={t('builder.profile.photograph')}
className="pl-6 w-full"
path="profile.photograph"
/>

View File

@ -1,17 +1,23 @@
import { Tooltip } from '@material-ui/core';
import React, { memo, useEffect } from 'react';
import { Link, scrollSpy } from 'react-scroll';
import { useTranslation } from 'react-i18next';
import styles from './SectionIcon.module.css';
const SectionIcon = ({ section, containerId, tooltipPlacement }) => {
const { id, name, icon: Icon } = section;
const { t } = useTranslation();
const { id, icon: Icon } = section;
useEffect(() => {
scrollSpy.update();
}, []);
return (
<Tooltip title={name} placement={tooltipPlacement} arrow>
<Tooltip
title={t(`builder.sections.${id}`)}
placement={tooltipPlacement}
arrow
>
<Link
spy
smooth

View File

@ -4,13 +4,14 @@ import ShortUniqueId from 'short-unique-id';
import React, { createContext, memo, useContext, useState } from 'react';
import UserContext from './UserContext';
import initialState from '../data/initialState.json';
import { getUnsplashPhoto } from '../utils';
const DEBOUNCE_WAIT_TIME = 4000;
const defaultState = {
isUpdating: false,
createResume: () => {},
duplicateResume: () => {},
createResume: async () => {},
duplicateResume: async () => {},
deleteResume: () => {},
getResume: async () => {},
getResumes: async () => {},
@ -39,8 +40,9 @@ const DatabaseProvider = ({ children }) => {
}
};
const createResume = ({ name }) => {
const createResume = async ({ name }) => {
const id = uuid();
const preview = await getUnsplashPhoto();
const createdAt = firebase.database.ServerValue.TIMESTAMP;
let firstName;
@ -55,6 +57,7 @@ const DatabaseProvider = ({ children }) => {
id,
name,
user: user.uid,
preview,
profile: {
...initialState.profile,
firstName: firstName || '',
@ -67,14 +70,16 @@ const DatabaseProvider = ({ children }) => {
firebase.database().ref(`resumes/${id}`).set(resume);
};
const duplicateResume = (originalResume) => {
const duplicateResume = async (originalResume) => {
const id = uuid();
const preview = await getUnsplashPhoto();
const createdAt = firebase.database.ServerValue.TIMESTAMP;
const resume = {
...originalResume,
id,
name: `${originalResume.name} Copy`,
preview,
createdAt,
updatedAt: createdAt,
};

View File

@ -1,5 +1,14 @@
import arrayMove from 'array-move';
import { clone, findIndex, get, isUndefined, merge, setWith } from 'lodash';
import {
clone,
findIndex,
get,
isUndefined,
merge,
setWith,
set,
has,
} from 'lodash';
import React, {
createContext,
memo,
@ -7,6 +16,7 @@ import React, {
useContext,
useReducer,
} from 'react';
import i18next from 'i18next';
import demoState from '../data/demoState.json';
import initialState from '../data/initialState.json';
import DatabaseContext from './DatabaseContext';
@ -73,6 +83,18 @@ const ResumeProvider = ({ children }) => {
debouncedUpdateResume(newState);
return newState;
case 'change_language':
newState = clone(state);
items = get(
i18next.getDataByLanguage(payload),
'translation.builder.sections',
);
Object.keys(items).forEach((key) => {
has(newState, `${key}.heading`) &&
set(newState, `${key}.heading`, items[key]);
});
return newState;
case 'reset_layout':
temp = get(state, 'metadata.template');
items = get(initialState, `metadata.layout.${temp}`);

View File

@ -0,0 +1,59 @@
import i18next from 'i18next';
import moment from 'moment';
import React, { createContext, memo, useEffect, useState } from 'react';
import themeConfig from '../data/themeConfig';
const defaultState = {
theme: 'Dark',
setTheme: () => {},
language: 'en',
setLanguage: () => {},
};
const SettingsContext = createContext(defaultState);
const SettingsProvider = ({ children }) => {
const [theme, setTheme] = useState(defaultState.theme);
const [language, setLanguage] = useState(defaultState.theme);
useEffect(() => {
const prefTheme = localStorage.getItem('theme') || defaultState.theme;
const prefLanguage =
localStorage.getItem('language') || defaultState.language;
setTheme(prefTheme);
setLanguage(prefLanguage);
}, []);
useEffect(() => {
localStorage.setItem('theme', theme);
const colorConfig = themeConfig[theme];
for (const [key, value] of Object.entries(colorConfig)) {
document.documentElement.style.setProperty(key, value);
}
}, [theme]);
useEffect(() => {
localStorage.setItem('language', language);
i18next.changeLanguage(language);
moment.locale(language);
}, [language]);
return (
<SettingsContext.Provider
value={{
theme,
setTheme,
language,
setLanguage,
}}
>
{children}
</SettingsContext.Provider>
);
};
export default SettingsContext;
const memoizedProvider = memo(SettingsProvider);
export { memoizedProvider as SettingsProvider };

View File

@ -1,47 +0,0 @@
import React, { createContext, memo, useEffect, useState } from 'react';
import themeConfig from '../data/themeConfig';
const defaultState = {
theme: 'Dark',
setTheme: () => {},
};
const ThemeContext = createContext(defaultState);
const ThemeProvider = ({ children }) => {
const [theme, setThemeX] = useState(defaultState.theme);
useEffect(() => {
const prefTheme = localStorage.getItem('theme') || defaultState.theme;
setThemeX(prefTheme);
}, []);
useEffect(() => {
const colorConfig = themeConfig[theme];
for (const [key, value] of Object.entries(colorConfig)) {
document.documentElement.style.setProperty(key, value);
}
}, [theme]);
const setTheme = (themeRef) => {
setThemeX(themeRef);
localStorage.setItem('theme', themeRef);
};
return (
<ThemeContext.Provider
value={{
theme,
setTheme,
}}
>
{children}
</ThemeContext.Provider>
);
};
export default ThemeContext;
const memoizedProvider = memo(ThemeProvider);
export { memoizedProvider as ThemeProvider };

View File

@ -1,8 +0,0 @@
const languageConfig = [
{
key: 'en',
name: 'English',
},
];
export default languageConfig;

View File

@ -1,6 +1,6 @@
import { AiFillSafetyCertificate, AiOutlineTwitter } from 'react-icons/ai';
import { BsTools } from 'react-icons/bs';
import { FaAward, FaUserFriends, FaProjectDiagram } from 'react-icons/fa';
import { FaAward, FaProjectDiagram, FaUserFriends } from 'react-icons/fa';
import {
IoLogoGameControllerB,
IoMdBriefcase,
@ -12,73 +12,61 @@ import ModalEvents from '../constants/ModalEvents';
export default [
{
id: 'profile',
name: 'Profile',
icon: MdPerson,
fixed: true,
},
{
id: 'social',
name: 'Social Network',
icon: AiOutlineTwitter,
event: ModalEvents.SOCIAL_MODAL,
fixed: true,
},
{
id: 'objective',
name: 'Objective',
icon: IoMdDocument,
},
{
id: 'work',
name: 'Work Experience',
icon: IoMdBriefcase,
event: ModalEvents.WORK_MODAL,
},
{
id: 'education',
name: 'Education',
icon: MdSchool,
event: ModalEvents.EDUCATION_MODAL,
},
{
id: 'projects',
name: 'Projects',
icon: FaProjectDiagram,
event: ModalEvents.PROJECT_MODAL,
},
{
id: 'awards',
name: 'Awards',
icon: FaAward,
event: ModalEvents.AWARD_MODAL,
},
{
id: 'certifications',
name: 'Certifications',
icon: AiFillSafetyCertificate,
event: ModalEvents.CERTIFICATION_MODAL,
},
{
id: 'skills',
name: 'Skills',
icon: BsTools,
event: ModalEvents.SKILL_MODAL,
},
{
id: 'hobbies',
name: 'Hobbies',
icon: IoLogoGameControllerB,
event: ModalEvents.HOBBY_MODAL,
},
{
id: 'languages',
name: 'Languages',
icon: MdTranslate,
event: ModalEvents.LANGUAGE_MODAL,
},
{
id: 'references',
name: 'References',
icon: FaUserFriends,
event: ModalEvents.REFERENCE_MODAL,
},

View File

@ -11,37 +11,30 @@ import {
export default [
{
id: 'templates',
name: 'Templates',
icon: MdStyle,
},
{
id: 'layout',
name: 'Layout',
icon: MdDashboard,
},
{
id: 'colors',
name: 'Colors',
icon: MdColorLens,
},
{
id: 'fonts',
name: 'Fonts',
icon: MdFontDownload,
},
{
id: 'actions',
name: 'Actions',
icon: MdImportExport,
},
{
id: 'settings',
name: 'Settings',
icon: MdSettings,
},
{
id: 'about',
name: 'About',
icon: MdInfo,
},
];

25
src/i18n/index.js Normal file
View File

@ -0,0 +1,25 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import resources from './locales';
const languages = [
{
code: 'en',
name: 'English (US)',
},
{
code: 'kn',
name: 'Kannada (ಕನ್ನಡ)',
},
];
i18n.use(initReactI18next).init({
resources,
lng: 'en',
debug: true,
fallbackLng: 'en',
});
export { languages };
export default i18n;

247
src/i18n/locales/en.json Normal file
View File

@ -0,0 +1,247 @@
{
"shared": {
"appName": "Reactive Resume",
"shortDescription": "A free and open source resume builder.",
"forms": {
"name": "Name",
"title": "Title",
"subtitle": "Subtitle",
"required": "required",
"website": "Website",
"date": "Date",
"position": "Position",
"startDate": "Start Date",
"endDate": "End Date",
"address": "Address",
"phone": "Phone Number",
"email": "Email Address",
"summary": "Summary",
"markdown": "This text block supports <1>markdown</1>.",
"validation": {
"min": "Please enter at least {{number}} characters.",
"dateRange": "End Date must be later than Start Date.",
"email": "Must be a valid email address.",
"required": "This is a required field.",
"url": "Must be a valid URL."
}
},
"buttons": {
"add": "Add",
"edit": "Edit",
"cancel": "Cancel",
"delete": "Delete",
"loading": "Loading...",
"confirmation": "Are you sure?",
"login": "Login",
"logout": "Logout"
}
},
"landing": {
"hero": {
"goToApp": "Go To App"
}
},
"dashboard": {
"title": "Dashboard",
"createResume": "Create Resume",
"editResume": "Edit Resume",
"lastUpdated": "Last Updated {{timestamp}}",
"toasts": {
"deleted": "{{name}} was deleted successfully"
},
"buttons": {
"duplicate": "Duplicate",
"rename": "Rename"
},
"helpText": "You are going to be creating a new resume from scratch, but first, let's give it a name. This can be the name of the role you want to apply for, or if you're making a resume for a friend, you could call it Alex's Resume."
},
"builder": {
"toasts": {
"doesNotExist": "The resume you were looking for does not exist anymore... or maybe it never did?",
"loadDemoData": "Not sure where to begin? Try loading demo data to see what Reactive Resume has to offer."
},
"sections": {
"profile": "Profile",
"social": "Social Network",
"objective": "Objective",
"work": "Work Experience",
"education": "Education",
"project": "Project",
"projects": "Projects",
"award": "Award",
"awards": "Awards",
"certification": "Certification",
"certifications": "Certifications",
"skill": "Skill",
"skills": "Skills",
"hobby": "Hobby",
"hobbies": "Hobbies",
"language": "Language",
"languages": "Languages",
"reference": "Reference",
"references": "References",
"templates": "Templates",
"layout": "Layout",
"colors": "Colors",
"fonts": "Fonts",
"actions": "Actions",
"settings": "Settings",
"about": "About"
},
"profile": {
"photograph": "Photograph",
"firstName": "First Name",
"lastName": "Last Name",
"address": {
"line1": "Address Line 1",
"line2": "Address Line 2",
"city": "City",
"pincode": "Pincode"
}
},
"social": {
"network": "Network",
"username": "Username",
"url": "URL"
},
"work": {
"company": "Company"
},
"education": {
"institution": "Institution",
"field": "Field of Study",
"degree": "Type of Degree",
"gpa": "GPA"
},
"awards": {
"awarder": "Awarder"
},
"certifications": {
"issuer": "Issuer"
},
"skills": {
"level": "Level"
},
"languages": {
"fluency": "Fluency"
},
"layout": {
"block": "Block",
"reset": "Reset Layout",
"text": "This template supports {{count}} blocks."
},
"colors": {
"primary": "Primary Color",
"text": "Text Color",
"background": "Background Color"
},
"actions": {
"import": {
"heading": "Import Your Resume",
"text": "You can import your information from various sources like JSON Resume or your LinkedIn to autofill most of the data for your resume.",
"button": "Import"
},
"export": {
"heading": "Export Your Resume",
"text": "Export your resume as a PDF to share with recruiters or a JSON that you will be able to import back onto this app on another computer.",
"button": "Export"
},
"share": {
"heading": "Share Your Resume",
"text": "The link below will be accessible publicly if you choose to share it, and viewers would see the latest version of your resume at any time."
},
"loadDemoData": {
"text": "Unclear on what to do with a fresh blank page? Load some demo data to see how a resume should look and you can start editing from there.",
"button": "Load Demo Data"
},
"resetEverything": {
"text": "Feels like you made too many mistakes? No worries, clear everything with just one click, but be careful if there are no backups.",
"button": "Reset Everything"
}
},
"settings": {
"theme": "Theme",
"language": "Language",
"translate": "If you would like to contribute by providing translations in your language, <1>please visit this link</1>.",
"dangerZone": {
"heading": "Danger Zone",
"text": " If you would like to delete your account and erase all your resumes, its just one button away. Please be weary as this is an irreversible process.",
"button": "Delete Account"
}
},
"about": {
"donate": {
"heading": "Donate to Reactive Resume",
"text": "I try to do what I can, but if you found the app helpful, or you're in a better position than the others who depend on this project for their first job, <1>please consider donating as little as $5 to help keep the project alive</1> :)",
"button": "Buy me a Coffee!"
},
"bugFeature": {
"heading": "Bug? Feature Request?",
"text": "Something halting your progress from making a resume? Found a pesky bug that just won't quit? Talk about it on the GitHub Issues section using the actions below.",
"button": "Raise an Issue"
},
"appreciate": {
"heading": "Loved Reactive Resume?",
"text": "I never get tired of hearing stories of how this app helped people, and if it helped you, or you just found Reactive Resume to be an awesome tool, do let me know. You can reach out to me on my website."
},
"sourceCode": {
"heading": "Source Code",
"text": "Want to run the project from its source? Are you a developer willing to contribute to the open-source development of this project? Click the button below.",
"button": "GitHub Repo"
},
"footer": "Made with Love by <1>Amruth Pillai</1>"
},
"tooltips": {
"uploadPhotograph": "Upload Photograph",
"backToDashboard": "Go Back to Dashboard"
},
"emptyList": "This list is empty."
},
"modals": {
"auth": {
"whoAreYou": "Who are you?",
"welcome": "Welcome, {{name}}!",
"loggedOutText": "Reactive Resume needs to know who you are so it can securely authenticate you into the app and show you only your information. Once you are in, you can start building your resume, editing it to add new skills or sharing it with the world!",
"loggedInText": "Awesome. Now that you've authenticated yourself, we can get on with the real reason you're here. Click on the Go to App button to start building your resume!",
"buttons": {
"google": "Sign in with Google",
"anonymous": "Sign in Anonymously"
}
},
"import": {
"button": "Select File",
"reactiveResume": {
"heading": "Import from Reactive Resume",
"text": "Reactive Resume has it's own schema format to make the most of all the customizable capabilities it has to offer. If you'd like to import a backup of your resume made with this app, just upload the file using the button below."
},
"jsonResume": {
"heading": "Import from JSON Resume",
"text": "JSON Resume is an open standard for resume schema structure. If you are one of the many enthusiasts who have their resume ready in this format, all it takes it just one click to get started with Reactive Resume."
},
"linkedIn": {
"heading": "Import from LinkedIn",
"text": "You can import a JSON that was exported from Reactive Resume by clicking on the button below and selecting the appropriate file."
}
},
"export": {
"printDialog": {
"heading": "Use Browser's Print Dialog",
"text": "For those of you who want a quick solution, you need not look any further than your browser. All you have to do is press Ctrl/Cmd + P and open up the print dialog on your browser and get your resume printed immediately.",
"button": "Print Resume"
},
"downloadPDF": {
"heading": "Download PDF",
"text": "These options allow you to print a single page, unconstrained version of your resume, perfect for those who have a lot of content. Alternatively, you could download a multi-page version of your resume as well with just one click.",
"buttons": {
"single": "Single Page Resume",
"multi": "Multi Page Resume"
}
},
"jsonFormat": {
"heading": "Export to JSON Format",
"text": "You can also export your data into JSON format for safe keeping so that you can easily import it back into Reactive Resume whenever you want to edit or generate a resume.",
"button": "Export JSON"
}
}
}
}

View File

@ -0,0 +1,7 @@
import en from './en.json';
import kn from './kn.json';
export default {
en: { translation: en },
kn: { translation: kn },
};

247
src/i18n/locales/kn.json Normal file
View File

@ -0,0 +1,247 @@
{
"shared": {
"appName": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭ",
"shortDescription": "ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲ ಪುನರಾರಂಭ ಬಿಲ್ಡರ್.",
"forms": {
"name": "ಹೆಸರು",
"title": "ಶೀರ್ಷಿಕೆ",
"subtitle": "ಉಪಶೀರ್ಷಿಕೆ",
"required": "ಅಗತ್ಯವಿದೆ",
"website": "ಜಾಲತಾಣ",
"date": "ದಿನಾಂಕ",
"position": "ಸ್ಥಾನ",
"startDate": "ಪ್ರಾರಂಭ ದಿನಾಂಕ",
"endDate": "ಅಂತಿಮ ದಿನಾಂಕ",
"address": "ವಿಳಾಸ",
"phone": "ದೂರವಾಣಿ ಸಂಖ್ಯೆ",
"email": "ಇಮೇಲ್ ವಿಳಾಸ",
"summary": "ಸಾರಾಂಶ",
"markdown": "ಈ ಪಠ್ಯ ಬ್ಲಾಕ್ <1> ಮಾರ್ಕ್‌ಡೌನ್ ಅನ್ನು ಬೆಂಬಲಿಸುತ್ತದೆ </1> .",
"validation": {
"min": "ದಯವಿಟ್ಟು ಕನಿಷ್ಠ {{number}} ಅಕ್ಷರಗಳನ್ನು ನಮೂದಿಸಿ.",
"dateRange": "ಅಂತಿಮ ದಿನಾಂಕವು ಪ್ರಾರಂಭ ದಿನಾಂಕಕ್ಕಿಂತ ನಂತರ ಇರಬೇಕು.",
"email": "ಮಾನ್ಯವಾದ ಇಮೇಲ್ ವಿಳಾಸವಾಗಿರಬೇಕು.",
"required": "ಇದು ಅಗತ್ಯವಾದ ಕ್ಷೇತ್ರವಾಗಿದೆ.",
"url": "ಮಾನ್ಯವಾದ URL ಆಗಿರಬೇಕು."
}
},
"buttons": {
"add": "ಸೇರಿಸಿ",
"edit": "ತಿದ್ದು",
"cancel": "ರದ್ದುಮಾಡಿ",
"delete": "ಅಳಿಸಿ",
"loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ ...",
"confirmation": "ನೀವು ಖಚಿತವಾಗಿರುವಿರಾ?",
"login": "ಲಾಗಿನ್ ಮಾಡಿ",
"logout": "ಲಾಗ್ ಔಟ್"
}
},
"landing": {
"hero": {
"goToApp": "ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಹೋಗಿ"
}
},
"dashboard": {
"title": "ಡ್ಯಾಶ್‌ಬೋರ್ಡ್",
"createResume": "ಪುನರಾರಂಭವನ್ನು ರಚಿಸಿ",
"editResume": "ಪುನರಾರಂಭವನ್ನು ಸಂಪಾದಿಸಿ",
"lastUpdated": "ಕೊನೆಯದಾಗಿ ನವೀಕರಿಸಲಾಗಿದೆ {{timestamp}}",
"toasts": {
"deleted": "{{name}} ಅನ್ನು ಯಶಸ್ವಿಯಾಗಿ ಅಳಿಸಲಾಗಿದೆ"
},
"buttons": {
"duplicate": "ನಕಲು",
"rename": "ಮರುಹೆಸರಿಸಿ"
},
"helpText": "ನೀವು ಮೊದಲಿನಿಂದ ಹೊಸ ಪುನರಾರಂಭವನ್ನು ರಚಿಸಲಿದ್ದೀರಿ, ಆದರೆ ಮೊದಲು, ಅದಕ್ಕೆ ಹೆಸರನ್ನು ನೀಡೋಣ. ಇದು ನೀವು ಅರ್ಜಿ ಸಲ್ಲಿಸಲು ಬಯಸುವ ಪಾತ್ರದ ಹೆಸರಾಗಿರಬಹುದು ಅಥವಾ ನೀವು ಸ್ನೇಹಿತರಿಗಾಗಿ ಪುನರಾರಂಭವನ್ನು ಮಾಡುತ್ತಿದ್ದರೆ, ನೀವು ಅದನ್ನು ಅಲೆಕ್ಸ್ ಪುನರಾರಂಭ ಎಂದು ಕರೆಯಬಹುದು."
},
"builder": {
"toasts": {
"doesNotExist": "ನೀವು ಹುಡುಕುತ್ತಿದ್ದ ಪುನರಾರಂಭವು ಇನ್ನು ಮುಂದೆ ಅಸ್ತಿತ್ವದಲ್ಲಿಲ್ಲ ... ಅಥವಾ ಬಹುಶಃ ಅದು ಎಂದಿಗೂ ಆಗಲಿಲ್ಲವೇ?",
"loadDemoData": "ಎಲ್ಲಿಂದ ಪ್ರಾರಂಭಿಸಬೇಕು ಎಂದು ಖಚಿತವಾಗಿಲ್ಲವೇ? ರಿಯಾಕ್ಟಿವ್ ಪುನರಾರಂಭವು ಏನು ನೀಡುತ್ತದೆ ಎಂಬುದನ್ನು ನೋಡಲು ಡೆಮೊ ಡೇಟಾವನ್ನು ಲೋಡ್ ಮಾಡಲು ಪ್ರಯತ್ನಿಸಿ."
},
"sections": {
"profile": "ಪ್ರೊಫೈಲ್",
"social": "ಸಾಮಾಜಿಕ ತಾಣ",
"objective": "ಉದ್ದೇಶ",
"work": "ಕೆಲಸದ ಅನುಭವ",
"education": "ಶಿಕ್ಷಣ",
"project": "ಯೋಜನೆ",
"projects": "ಯೋಜನೆಗಳು",
"award": "ಪ್ರಶಸ್ತಿ",
"awards": "ಪ್ರಶಸ್ತಿಗಳು",
"certification": "ಪ್ರಮಾಣೀಕರಣ",
"certifications": "ಪ್ರಮಾಣೀಕರಣಗಳು",
"skill": "ಕೌಶಲ್ಯ",
"skills": "ಕೌಶಲ್ಯಗಳು",
"hobby": "ಹವ್ಯಾಸ",
"hobbies": "ಹವ್ಯಾಸಗಳು",
"language": "ಭಾಷೆ",
"languages": "ಭಾಷೆಗಳು",
"reference": "ಉಲ್ಲೇಖ",
"references": "ಉಲ್ಲೇಖಗಳು",
"templates": "ಟೆಂಪ್ಲೇಟ್‌ಗಳು",
"layout": "ಲೆಔಟ್",
"colors": "ಬಣ್ಣಗಳು",
"fonts": "ಫಾಂಟ್‌ಗಳು",
"actions": "ಕ್ರಿಯೆಗಳು",
"settings": "ಸಂಯೋಜನೆಗಳು",
"about": "ಬಗ್ಗೆ"
},
"profile": {
"firstName": "ಮೊದಲ ಹೆಸರು",
"lastName": "ಕೊನೆಯ ಹೆಸರು",
"address": {
"line1": "ವಿಳಾಸ ಸಾಲು 1",
"line2": "ವಿಳಾಸ ಸಾಲು 2",
"city": "ನಗರ",
"pincode": "ಪಿನ್ಕೋಡ್"
},
"photograph": ""
},
"social": {
"network": "ನೆಟ್‌ವರ್ಕ್",
"username": "ಬಳಕೆದಾರ ಹೆಸರು",
"url": "URL"
},
"work": {
"company": "ಕಂಪನಿ"
},
"education": {
"institution": "ಸಂಸ್ಥೆ",
"field": "ಅಧ್ಯಯನದ ಕ್ಷೇತ್ರ",
"degree": "ಪದವಿ ಪ್ರಕಾರ",
"gpa": "ಜಿಪಿಎ"
},
"awards": {
"awarder": "ಪ್ರಶಸ್ತಿ ಪುರಸ್ಕೃತ"
},
"certifications": {
"issuer": "ನೀಡುವವರು"
},
"skills": {
"level": "ಮಟ್ಟ"
},
"languages": {
"fluency": "ನಿರರ್ಗಳತೆ"
},
"layout": {
"block": "ನಿರ್ಬಂಧಿಸಿ",
"reset": "ವಿನ್ಯಾಸವನ್ನು ಮರುಹೊಂದಿಸಿ",
"text": "ಈ ಟೆಂಪ್ಲೇಟ್ {{count}} ಬ್ಲಾಕ್ಗಳನ್ನು ಬೆಂಬಲಿಸುತ್ತದೆ."
},
"colors": {
"primary": "ಪ್ರಾಥಮಿಕ ಬಣ್ಣ",
"text": "ಪಠ್ಯ ಬಣ್ಣ",
"background": "ಹಿನ್ನೆಲೆ ಬಣ್ಣ"
},
"actions": {
"import": {
"heading": "ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ಆಮದು ಮಾಡಿ",
"text": "ನಿಮ್ಮ ಪುನರಾರಂಭಕ್ಕಾಗಿ ಹೆಚ್ಚಿನ ಡೇಟಾವನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಭರ್ತಿ ಮಾಡಲು ನಿಮ್ಮ ಮಾಹಿತಿಯನ್ನು JSON ಪುನರಾರಂಭ ಅಥವಾ ನಿಮ್ಮ ಲಿಂಕ್ಡ್‌ಇನ್‌ನಂತಹ ವಿವಿಧ ಮೂಲಗಳಿಂದ ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು.",
"button": "ಆಮದು"
},
"export": {
"heading": "ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ರಫ್ತು ಮಾಡಿ",
"text": "ನೇಮಕಾತಿಗಾರರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಲು ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ಪಿಡಿಎಫ್ ಆಗಿ ರಫ್ತು ಮಾಡಿ ಅಥವಾ ಇನ್ನೊಂದು ಕಂಪ್ಯೂಟರ್‌ನಲ್ಲಿ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಮರಳಿ ಆಮದು ಮಾಡಲು ನಿಮಗೆ ಸಾಧ್ಯವಾಗುತ್ತದೆ.",
"button": "ರಫ್ತು ಮಾಡಿ"
},
"share": {
"heading": "ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ",
"text": "ನೀವು ಅದನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಆರಿಸಿದರೆ ಕೆಳಗಿನ ಲಿಂಕ್ ಅನ್ನು ಸಾರ್ವಜನಿಕವಾಗಿ ಪ್ರವೇಶಿಸಬಹುದು ಮತ್ತು ವೀಕ್ಷಕರು ನಿಮ್ಮ ಪುನರಾರಂಭದ ಇತ್ತೀಚಿನ ಆವೃತ್ತಿಯನ್ನು ಯಾವುದೇ ಸಮಯದಲ್ಲಿ ನೋಡುತ್ತಾರೆ."
},
"loadDemoData": {
"text": "ಹೊಸ ಖಾಲಿ ಪುಟದೊಂದಿಗೆ ಏನು ಮಾಡಬೇಕೆಂದು ಸ್ಪಷ್ಟವಾಗಿಲ್ಲವೇ? ಪುನರಾರಂಭವು ಹೇಗೆ ಕಾಣುತ್ತದೆ ಎಂಬುದನ್ನು ನೋಡಲು ಕೆಲವು ಡೆಮೊ ಡೇಟಾವನ್ನು ಲೋಡ್ ಮಾಡಿ ಮತ್ತು ನೀವು ಅಲ್ಲಿಂದ ಸಂಪಾದನೆಯನ್ನು ಪ್ರಾರಭಿಸಬಹ<E0B2AC><E0B2B9>ದು.",
"button": "ಡೆಮೊ ಡೇಟಾವನ್ನು ಲೋಡ್ ಮಾಡಿ"
},
"resetEverything": {
"text": "ನೀವು ತುಂಬಾ ತಪ್ಪುಗಳನ್ನು ಮಾಡಿದ್ದೀರಿ ಎಂದು ಅನಿಸುತ್ತದೆಯೇ? ಚಿಂತಿಸಬೇಡಿ, ಕೇವಲ ಒಂದು ಕ್ಲಿಕ್‌ನಲ್ಲಿ ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ, ಆದರೆ ಬ್ಯಾಕಪ್‌ಗಳಿಲ್ಲದಿದ್ದರೆ ಜಾಗರೂಕರಾಗಿರಿ.",
"button": "ಎಲ್ಲವನ್ನೂ ಮರುಹೊಂದಿಸಿ"
}
},
"settings": {
"theme": "ಥೀಮ್",
"language": "ಭಾಷೆ",
"translate": "ನಿಮ್ಮ ಭಾಷೆಯಲ್ಲಿ ಅನುವಾದಗಳನ್ನು ಒದಗಿಸುವ ಮೂಲಕ ನೀವು ಕೊಡುಗೆ ನೀಡಲು ಬಯಸಿದರೆ, <1> ದಯವಿಟ್ಟು ಈ ಲಿಂಕ್‌ಗೆ ಭೇಟಿ ನೀಡಿ </1> .",
"dangerZone": {
"heading": "ಅಪಾಯ ವಲಯ",
"text": "ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಅಳಿಸಲು ಮತ್ತು ನಿಮ್ಮ ಎಲ್ಲಾ ಮುಂದುವರಿಕೆಗಳನ್ನು ಅಳಿಸಲು ನೀವು ಬಯಸಿದರೆ, ಅದು ಕೇವಲ ಒಂದು ಬಟನ್ ದೂರದಲ್ಲಿದೆ. ಇದು ಬದಲಾಯಿಸಲಾಗದ ಪ್ರಕ್ರಿಯೆಯಾಗಿರುವುದರಿಂದ ದಯವಿಟ್ಟು ದಣಿದಿರಿ.",
"button": "ಖಾತೆಯನ್ನು ಅಳಿಸಿ"
}
},
"about": {
"donate": {
"heading": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭಕ್ಕೆ ದಾನ ಮಾಡಿ",
"text": "ನಾನು ಏನು ಮಾಡಬಹುದೆಂಬುದನ್ನು ಮಾಡಲು ನಾನು ಪ್ರಯತ್ನಿಸುತ್ತೇನೆ, ಆದರೆ ನೀವು ಅಪ್ಲಿಕೇಶನ್ ಸಹಾಯಕವಾಗಿದೆಯೆಂದು ಕಂಡುಕೊಂಡರೆ ಅಥವಾ ಅವರ ಮೊದಲ ಕೆಲಸಕ್ಕಾಗಿ ಈ ಯೋಜನೆಯನ್ನು ಅವಲಂಬಿಸಿರುವ ಇತರರಿಗಿಂತ ನೀವು ಉತ್ತಮ ಸ್ಥಾನದಲ್ಲಿದ್ದರೆ, <1> ದಯವಿಟ್ಟು ಸಹಾಯ ಮಾಡಲು $ 5 ರಷ್ಟನ್ನು ದಾನ ಮಾಡುವುದನ್ನು ಪರಿಗಣಿಸಿ ಯೋಜನೆ ಜೀವಂತವಾಗಿದೆ </1> :)",
"button": "ನನಗೆ ಕಾಫಿ ಖರೀದಿಸಿ!"
},
"bugFeature": {
"heading": "ದೋಷ? ವೈಶಿಷ್ಟ್ಯ ವಿನಂತಿ?",
"text": "ಪುನರಾರಂಭವನ್ನು ಮಾಡುವುದರಿಂದ ನಿಮ್ಮ ಪ್ರಗತಿಯನ್ನು ಏನಾದರೂ ತಡೆಯುತ್ತೀರಾ? ತೊರೆಯದಂತಹ ತೊಂದರೆ ದೋಷ ಕಂಡುಬಂದಿದೆ? ಕೆಳಗಿನ ಕ್ರಿಯೆಗಳನ್ನು ಬಳಸಿಕೊಂಡು ಗಿಟ್‌ಹಬ್ ಸಮಸ್ಯೆಗಳ ವಿಭಾಗದಲ್ಲಿ ಇದರ ಬಗ್ಗೆ ಮಾತನಾಡಿ.",
"button": "ಸಮಸ್ಯೆಯನ್ನು ಹೆಚ್ಚಿಸಿ"
},
"appreciate": {
"heading": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭವನ್ನು ಇಷ್ಟಪಟ್ಟಿದ್ದೀರಾ?",
"text": "ಈ ಅಪ್ಲಿಕೇಶನ್ ಜನರಿಗೆ ಹೇಗೆ ಸಹಾಯ ಮಾಡಿದೆ ಎಂಬ ಕಥೆಗಳನ್ನು ಕೇಳಿದಾಗ ನಾನು ಎಂದಿಗೂ ಸುಸ್ತಾಗುವುದಿಲ್ಲ, ಮತ್ತು ಅದು ನಿಮಗೆ ಸಹಾಯ ಮಾಡಿದರೆ ಅಥವಾ ರಿಯಾಕ್ಟಿವ್ ಪುನರಾರಂಭವನ್ನು ಅದ್ಭುತ ಸಾಧನವೆಂದು ನೀವು ಕಂಡುಕೊಂಡಿದ್ದರೆ, ನನಗೆ ತಿಳಿಸಿ. ನನ್ನ ವೆಬ್‌ಸೈಟ್‌ನಲ್ಲಿ ನೀವು ನನ್ನನ್ನು ಸಂಪರ್ಕಿಸಬಹುದು."
},
"sourceCode": {
"heading": "ಮೂಲ ಕೋಡ್",
"text": "ಯೋಜನೆಯನ್ನು ಅದರ ಮೂಲದಿಂದ ಚಲಾಯಿಸಲು ಬಯಸುವಿರಾ? ಈ ಯೋಜನೆಯ ಮುಕ್ತ ಮೂಲ ಅಭಿವೃದ್ಧಿಗೆ ಕೊಡುಗೆ ನೀಡಲು ನೀವು ಸಿದ್ಧರಿದ್ದೀರಾ? ಕೆಳಗಿನ ಬಟನ್ ಕ್ಲಿಕ್ ಮಾಡಿ.",
"button": "ಗಿಟ್‌ಹಬ್ ರೆಪೊ"
},
"footer": "<1> ಅಮೃತ್ ಪಿಳ್ಳೈ ಅವರಿಂದ ಪ್ರೀತಿಯಿಂದ ಮಾಡಲ್ಪಟ್ಟಿದೆ </1>"
},
"tooltips": {
"uploadPhotograph": "Ograph ಾಯಾಚಿತ್ರವನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ",
"backToDashboard": "ಡ್ಯಾಶ್‌ಬೋರ್ಡ್‌ಗೆ ಹಿಂತಿರುಗಿ"
},
"emptyList": "ಈ ಪಟ್ಟಿ ಖಾಲಿಯಾಗಿದೆ."
},
"modals": {
"auth": {
"whoAreYou": "ನೀವು ಯಾರು?",
"welcome": "ಸ್ವಾಗತ, {{name}}!",
"loggedOutText": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭವು ನೀವು ಯಾರೆಂದು ತಿಳಿದುಕೊಳ್ಳಬೇಕು ಆದ್ದರಿಂದ ಅದು ನಿಮ್ಮನ್ನು ಅಪ್ಲಿಕೇಶನ್‌ನಲ್ಲಿ ಸುರಕ್ಷಿತವಾಗಿ ದೃ ate ೀಕರಿಸುತ್ತದೆ ಮತ್ತು ನಿಮ್ಮ ಮಾಹಿತಿಯನ್ನು ಮಾತ್ರ ತೋರಿಸುತ್ತದೆ. ನೀವು ಪ್ರವೇಶಿಸಿದ ನಂತರ, ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ನಿರ್ಮಿಸಲು, ಹೊಸ ಕೌಶಲ್ಯಗಳನ್ನು ಸೇರಿಸಲು ಅದನ್ನು ಸಂಪಾದಿಸಲು ಅಥವಾ ಅದನ್ನು ಜಗತ್ತಿನೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಲು ನೀವು ಪ್ರಾರಂಭಿಸಬಹುದು!",
"loggedInText": "ಅದ್ಭುತ. ಈಗ ನೀವು ನಿಮ್ಮನ್ನು ದೃ ated ೀಕರಿಸಿದ್ದೀರಿ, ನೀವು ಇಲ್ಲಿರುವ ನಿಜವಾದ ಕಾರಣವನ್ನು ನಾವು ಪಡೆಯಬಹುದು. ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ನಿರ್ಮಿಸಲು ಪ್ರಾರಂಭಿಸಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಹೋಗಿ ಬಟನ್ ಕ್ಲಿಕ್ ಮಾಡಿ!",
"buttons": {
"google": "Google ನೊಂದಿಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ",
"anonymous": "ಅನಾಮಧೇಯವಾಗಿ ಸೈನ್ ಇನ್ ಮಾಡಿ"
}
},
"import": {
"button": "ಫೈಲ್ ಆಯ್ಕೆಮಾಡಿ",
"reactiveResume": {
"heading": "ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭದಿಂದ ಆಮದು ಮಾಡಿ",
"text": "ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ ತನ್ನದೇ ಆದ ಸ್ಕೀಮಾ ಸ್ವರೂಪವನ್ನು ಹೊಂದಿದ್ದು ಅದು ನೀಡುವ ಎಲ್ಲಾ ಗ್ರಾಹಕೀಯಗೊಳಿಸಬಹುದಾದ ಸಾಮರ್ಥ್ಯಗಳನ್ನು ಹೆಚ್ಚು ಮಾಡುತ್ತದೆ. ಈ ಅಪ್ಲಿಕೇಶನ್‌ನೊಂದಿಗೆ ಮಾಡಿದ ನಿಮ್ಮ ಪುನರಾರಂಭದ ಬ್ಯಾಕಪ್ ಅನ್ನು ಆಮದು ಮಾಡಲು ನೀವು ಬಯಸಿದರೆ, ಕೆಳಗಿನ ಬಟನ್ ಬಳಸಿ ಫೈಲ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡಿ."
},
"jsonResume": {
"heading": "JSON ಪುನರಾರಂಭದಿಂದ ಆಮದು ಮಾಡಿ",
"text": "JSON ಪುನರಾರಂಭವು ಪುನರಾರಂಭ ಸ್ಕೀಮಾ ರಚನೆಗೆ ಮುಕ್ತ ಮಾನದಂಡವಾಗಿದೆ. ಈ ಸ್ವರೂಪದಲ್ಲಿ ತಮ್ಮ ಪುನರಾರಂಭವನ್ನು ಸಿದ್ಧಪಡಿಸಿದ ಅನೇಕ ಉತ್ಸಾಹಿಗಳಲ್ಲಿ ನೀವು ಒಬ್ಬರಾಗಿದ್ದರೆ, ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭದೊಂದಿಗೆ ಪ್ರಾರಂಭಿಸಲು ಕೇವಲ ಒಂದು ಕ್ಲಿಕ್ ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ."
},
"linkedIn": {
"heading": "ಲಿಕ್ಡ್ಇನ<E0B287><E0B2A8>ನಿದ ಆಮದು ಮಾಡಿ",
"text": "ಕೆಳಗಿನ ಬಟನ್ ಕ್ಲಿಕ್ ಮಾಡಿ ಮತ್ತು ಸೂಕ್ತವಾದ ಫೈಲ್ ಅನ್ನು ಆರಿಸುವ ಮೂಲಕ ರಿಯಾಕ್ಟಿವ್ ಪುನರಾರಂಭದಿಂದ ರಫ್ತು ಮಾಡಲಾದ JSON ಅನ್ನು ನೀವು ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು."
}
},
"export": {
"printDialog": {
"heading": "ಬ್ರೌಸರ್‌ನ ಮುದ್ರಣ ಸಂವಾದವನ್ನು ಬಳಸಿ",
"text": "ತ್ವರಿತ ಪರಿಹಾರವನ್ನು ಬಯಸುವ ನಿಮ್ಮಲ್ಲಿ, ನಿಮ್ಮ ಬ್ರೌಸರ್‌ಗಿಂತ ಹೆಚ್ಚಿನದನ್ನು ನೀವು ನೋಡಬೇಕಾಗಿಲ್ಲ. ನೀವು ಮಾಡಬೇಕಾಗಿರುವುದು Ctrl / Cmd + P ಅನ್ನು ಒತ್ತಿ ಮತ್ತು ನಿಮ್ಮ ಬ್ರೌಸರ್‌ನಲ್ಲಿ ಮುದ್ರಣ ಸಂವಾದವನ್ನು ತೆರೆಯಿರಿ ಮತ್ತು ನಿಮ್ಮ ಪುನರಾರಂಭವನ್ನು ತಕ್ಷಣ ಮುದ್ರಿಸಿ.",
"button": "ಪುನರಾರಂಭವನ್ನು ಮುದ್ರಿಸಿ"
},
"downloadPDF": {
"heading": "ಪಿಡಿಎಫ್ ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
"text": "ಈ ಆಯ್ಕೆಗಳು ಒಂದೇ ಪುಟವನ್ನು ಮುದ್ರಿಸಲು ನಿಮಗೆ ಅನುವು ಮಾಡಿಕೊಡುತ್ತದೆ, ನಿಮ್ಮ ಪುನರಾರಂಭದ ನಿರ್ಬಂಧಿಸದ ಆವೃತ್ತಿ, ಬಹಳಷ್ಟು ವಿಷಯವನ್ನು ಹೊಂದಿರುವವರಿಗೆ ಇದು ಸೂಕ್ತವಾಗಿದೆ. ಪರ್ಯಾಯವಾಗಿ, ನಿಮ್ಮ ಪುನರಾರಂಭದ ಬಹು-ಪುಟ ಆವೃತ್ತಿಯನ್ನು ನೀವು ಕೇವಲ ಒಂದು ಕ್ಲಿಕ್‌ನಲ್ಲಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಬಹುದು.",
"buttons": {
"single": "ಏಕ ಪುಟ ಪುನರಾರಂಭ",
"multi": "ಬಹು ಪುಟ ಪುನರಾರಂಭ"
}
},
"jsonFormat": {
"heading": "JSON ಸ್ವರೂಪಕ್ಕೆ ರಫ್ತು ಮಾಡಿ",
"text": "ಸುರಕ್ಷಿತವಾಗಿಡಲು ನಿಮ್ಮ ಡೇಟಾವನ್ನು ನೀವು JSON ಸ್ವರೂಪಕ್ಕೆ ರಫ್ತು ಮಾಡಬಹುದು, ಇದರಿಂದಾಗಿ ನೀವು ಪುನರಾರಂಭವನ್ನು ಸಂಪಾದಿಸಲು ಅಥವಾ ಉತ್ಪಾದಿಸಲು ಬಯಸಿದಾಗಲೆಲ್ಲಾ ಅದನ್ನು ಸುಲಭವಾಗಿ ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕ ಪುನರಾರಂಭಕ್ಕೆ ಆಮದು ಮಾಡಿಕೊಳ್ಳಬಹುದು.",
"button": "JSON ರಫ್ತು ಮಾಡಿ"
}
}
}
}

View File

@ -1,11 +1,13 @@
import { navigate } from 'gatsby';
import React, { memo, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../components/shared/Button';
import ModalContext from '../contexts/ModalContext';
import UserContext from '../contexts/UserContext';
import BaseModal from './BaseModal';
const AuthModal = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [isLoadingGoogle, setLoadingGoogle] = useState(false);
const [isLoadingAnonymous, setLoadingAnonymous] = useState(false);
@ -39,20 +41,20 @@ const AuthModal = () => {
};
const getTitle = () =>
user ? `Welcome, ${user.displayName || 'Agent 47'}` : 'Who are you?';
user
? t('modals.auth.welcome', { name: user.displayName || 'Agent 47' })
: t('modals.auth.whoAreYou');
const getMessage = () =>
user
? `Awesome. Now that you've authenticated yourself, we can get on with the real reason you're here. Click on the Go to App button to start building your resume!`
: `Reactive Resume needs to know who you are so it can securely authenticate you into the app and show you only your information. Once you are in, you can start building your resume, editing it to add new skills or sharing it with the world!`;
user ? t('modals.auth.loggedInText') : t('modals.auth.loggedOutText');
const loggedInAction = (
<>
<Button outline className="mr-8" onClick={logout}>
Logout
{t('shared.buttons.logout')}
</Button>
<Button title="" onClick={handleGotoApp}>
Go to App
{t('landing.hero.goToApp')}
</Button>
</>
);
@ -60,14 +62,14 @@ const AuthModal = () => {
const loggedOutAction = (
<div className="flex">
<Button isLoading={isLoadingGoogle} onClick={handleSignInWithGoogle}>
Sign in with Google
{t('modals.auth.buttons.google')}
</Button>
<Button
className="ml-8"
isLoading={isLoadingAnonymous}
onClick={handleSignInAnonymously}
>
Sign in Anonymously
{t('modals.auth.buttons.anonymous')}
</Button>
</div>
);

View File

@ -4,6 +4,7 @@ import Modal from '@material-ui/core/Modal';
import { isFunction } from 'lodash';
import React, { forwardRef, memo, useImperativeHandle } from 'react';
import { MdClose } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import Button from '../components/shared/Button';
import { handleKeyUp } from '../utils';
import styles from './BaseModal.module.css';
@ -11,6 +12,7 @@ import styles from './BaseModal.module.css';
const BaseModal = forwardRef(
({ title, state, children, action, hideActions = false, onDestroy }, ref) => {
const [open, setOpen] = state;
const { t } = useTranslation();
const handleClose = () => {
setOpen(false);
@ -47,7 +49,7 @@ const BaseModal = forwardRef(
{!hideActions && (
<div className={styles.actions}>
<Button outline className="mr-8" onClick={handleClose}>
Cancel
{t('shared.buttons.cancel')}
</Button>
{action}

View File

@ -2,6 +2,7 @@ import { useFormikContext } from 'formik';
import { isEmpty, isFunction } from 'lodash';
import React, { memo, useContext, useEffect, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useTranslation } from 'react-i18next';
import Button from '../components/shared/Button';
import ModalContext from '../contexts/ModalContext';
import { useDispatch } from '../contexts/ResumeContext';
@ -19,9 +20,11 @@ const DataModal = ({
}) => {
const modalRef = useRef(null);
const dispatch = useDispatch();
const { t } = useTranslation();
const [data, setData] = useState(null);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [isEditMode, setEditMode] = useState(false);
const { emitter } = useContext(ModalContext);
@ -41,11 +44,13 @@ const DataModal = ({
}, [data]);
const onSubmit = async (newData) => {
setLoading(true);
if (isEmpty(await validateForm())) {
if (isEditMode) {
if (data !== newData) {
isFunction(onEdit)
? onEdit(newData)
? await onEdit(newData)
: dispatch({
type: 'on_edit_item',
payload: {
@ -58,7 +63,7 @@ const DataModal = ({
newData.id = uuidv4();
isFunction(onCreate)
? onCreate(newData)
? await onCreate(newData)
: dispatch({
type: 'on_add_item',
payload: {
@ -68,6 +73,7 @@ const DataModal = ({
});
}
setLoading(false);
modalRef.current.handleClose();
}
};
@ -80,7 +86,7 @@ const DataModal = ({
const submitAction = (
<Button type="submit" onClick={() => onSubmit(values)}>
{getTitle}
{loading ? t('shared.buttons.loading') : getTitle}
</Button>
);

View File

@ -1,36 +1,27 @@
import { Formik } from 'formik';
import React, { memo, useContext } from 'react';
import * as Yup from 'yup';
import { useTranslation } from 'react-i18next';
import Input from '../components/shared/Input';
import ModalEvents from '../constants/ModalEvents';
import DatabaseContext from '../contexts/DatabaseContext';
import { getFieldProps } from '../utils';
import DataModal from './DataModal';
import leftSections from '../data/leftSections';
const initialValues = {
name: '',
metadata: {
template: 'onyx',
font: 'Montserrat',
layout: [leftSections.map(({ id, name }) => ({ id, name }))],
colors: {
text: '#444444',
primary: '#5875DB',
background: '#FFFFFF',
},
},
};
const schema = Yup.object().shape({
name: Yup.string()
.min(5, 'Please enter at least 5 characters.')
.required('This is a required field.'),
});
const ResumeModal = () => {
const { t } = useTranslation();
const { createResume, updateResume } = useContext(DatabaseContext);
const schema = Yup.object().shape({
name: Yup.string()
.min(5, t('shared.forms.validation.min', { number: 5 }))
.required(t('shared.forms.validation.required')),
});
return (
<Formik
validateOnBlur
@ -39,28 +30,22 @@ const ResumeModal = () => {
>
{(formik) => (
<DataModal
name="Resume"
title={{
create: 'Create Resume',
edit: 'Edit Resume',
create: t('dashboard.createResume'),
edit: t('dashboard.editResume'),
}}
onEdit={updateResume}
onCreate={createResume}
event={ModalEvents.CREATE_RESUME_MODAL}
>
<Input
label="Name"
label={t('shared.forms.name')}
className="mb-8"
placeholder="Full Stack Web Developer"
{...getFieldProps(formik, schema, 'name')}
/>
<p className="leading-loose">
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
to apply for, or if you&apos;re making a resume for a friend, you
could call it Alex&apos;s Resume.
</p>
<p className="leading-loose">{t('dashboard.helpText')}</p>
</DataModal>
)}
</Formik>

View File

@ -1,5 +1,6 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
@ -13,14 +14,16 @@ const initialValues = {
summary: '',
};
const schema = Yup.object().shape({
title: Yup.string().required('This is a required field.'),
awarder: Yup.string().required('This is a required field.'),
date: Yup.date().max(new Date()),
summary: Yup.string(),
});
const AwardModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
title: Yup.string().required(t('shared.forms.validation.required')),
awarder: Yup.string().required(t('shared.forms.validation.required')),
date: Yup.date().max(new Date()),
summary: Yup.string(),
});
return (
<Formik
validateOnBlur
@ -29,33 +32,33 @@ const AwardModal = () => {
>
{(formik) => (
<DataModal
name="Award"
name={t('builder.sections.award')}
path="awards.items"
event={ModalEvents.AWARD_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Title"
label={t('shared.forms.title')}
className="col-span-2"
placeholder="Intl. Flutter Hackathon '19"
{...getFieldProps(formik, schema, 'title')}
/>
<Input
label="Awarder"
label={t('builder.awards.awarder')}
placeholder="Google"
{...getFieldProps(formik, schema, 'awarder')}
/>
<Input
type="date"
label="Date"
label={t('shared.forms.date')}
{...getFieldProps(formik, schema, 'date')}
/>
<Input
type="textarea"
label="Summary"
label={t('shared.forms.summary')}
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>

View File

@ -1,5 +1,6 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
@ -13,14 +14,16 @@ const initialValues = {
summary: '',
};
const schema = Yup.object().shape({
title: Yup.string().required('This is a required field.'),
issuer: Yup.string().required('This is a required field.'),
date: Yup.date().max(new Date()),
summary: Yup.string(),
});
const CertificateModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
title: Yup.string().required(t('shared.forms.validation.required')),
issuer: Yup.string().required(t('shared.forms.validation.required')),
date: Yup.date().max(new Date()),
summary: Yup.string(),
});
return (
<Formik
validateOnBlur
@ -29,33 +32,33 @@ const CertificateModal = () => {
>
{(formik) => (
<DataModal
name="Certificate"
name={t('builder.sections.certification')}
path="certifications.items"
event={ModalEvents.CERTIFICATION_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Title"
label={t('shared.forms.title')}
className="col-span-2"
placeholder="CCNP"
{...getFieldProps(formik, schema, 'title')}
/>
<Input
label="Issuer"
label={t('builder.certifications.issuer')}
placeholder="Cisco Systems"
{...getFieldProps(formik, schema, 'issuer')}
/>
<Input
type="date"
label="Date"
label={t('shared.forms.date')}
{...getFieldProps(formik, schema, 'date')}
/>
<Input
type="textarea"
label="Summary"
label={t('shared.forms.summary')}
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>

View File

@ -1,6 +1,7 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import * as Yup from 'yup';
import { useTranslation } from 'react-i18next';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils';
@ -16,22 +17,27 @@ const initialValues = {
summary: '',
};
const schema = Yup.object().shape({
institution: Yup.string().required('This is a required field.'),
field: Yup.string().required('This is a required field.'),
degree: Yup.string(),
gpa: Yup.string(),
startDate: Yup.date().required('This is a required field.'),
endDate: Yup.date().when(
'startDate',
(startDate, yupSchema) =>
startDate &&
yupSchema.min(startDate, 'End Date must be later than Start Date'),
),
summary: Yup.string().min(10, 'Please enter at least 10 characters.'),
});
const EducationModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
institution: Yup.string().required(t('shared.forms.validation.required')),
field: Yup.string().required(t('shared.forms.validation.required')),
degree: Yup.string(),
gpa: Yup.string(),
startDate: Yup.date().required(t('shared.forms.validation.required')),
endDate: Yup.date().when(
'startDate',
(startDate, yupSchema) =>
startDate &&
yupSchema.min(startDate, t('shared.forms.validation.dateRange')),
),
summary: Yup.string().min(
10,
t('shared.forms.validation.min', { number: 10 }),
),
});
return (
<Formik
validateOnBlur
@ -40,54 +46,54 @@ const EducationModal = () => {
>
{(formik) => (
<DataModal
name="Education"
name={t('builder.sections.education')}
path="education.items"
event={ModalEvents.EDUCATION_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Institution"
label={t('builder.education.institution')}
className="col-span-2"
placeholder="Dayananda Sagar College of Engineering"
{...getFieldProps(formik, schema, 'institution')}
/>
<Input
label="Field of Study"
label={t('builder.education.field')}
className="col-span-2"
placeholder="Computer Science &amp; Engineering"
{...getFieldProps(formik, schema, 'field')}
/>
<Input
label="Degree Type"
label={t('builder.education.degree')}
placeholder="Bachelor's Degree"
{...getFieldProps(formik, schema, 'degree')}
/>
<Input
label="GPA"
label={t('builder.education.gpa')}
placeholder="8.8"
{...getFieldProps(formik, schema, 'gpa')}
/>
<Input
type="date"
label="Start Date"
label={t('shared.forms.startDate')}
placeholder="6th August 208"
{...getFieldProps(formik, schema, 'startDate')}
/>
<Input
type="date"
label="End Date"
label={t('shared.forms.endDate')}
placeholder="6th August 208"
{...getFieldProps(formik, schema, 'endDate')}
/>
<Input
type="textarea"
label="Summary"
label={t('shared.forms.summary')}
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>

View File

@ -1,6 +1,7 @@
import firebase from 'gatsby-plugin-firebase';
import { clone } from 'lodash';
import React, { memo, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaPrint } from 'react-icons/fa';
import Button from '../../components/shared/Button';
import ModalContext from '../../contexts/ModalContext';
@ -10,6 +11,7 @@ import BaseModal from '../BaseModal';
const ExportModal = () => {
const state = useSelector();
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [isLoadingSingle, setLoadingSingle] = useState(false);
const [isLoadingMulti, setLoadingMulti] = useState(false);
@ -72,35 +74,31 @@ const ExportModal = () => {
};
return (
<BaseModal hideActions state={[open, setOpen]} title="Export Your Resume">
<BaseModal
hideActions
state={[open, setOpen]}
title={t('builder.actions.export.heading')}
>
<div>
<h5 className="text-xl font-semibold mb-4">
Use Browser&apos;s Print Dialog
{t('modals.export.printDialog.heading')}
</h5>
<p className="leading-loose">
For those of you who want a quick solution, you need not look any
further than your browser. All you have to do is press Ctrl/Cmd + P
and open up the print dialog on your browser and get your resume
printed immediately.
</p>
<p className="leading-loose">{t('modals.export.printDialog.text')}</p>
<Button icon={FaPrint} className="mt-5" onClick={handleOpenPrintDialog}>
Print Resume
{t('modals.export.printDialog.button')}
</Button>
</div>
<hr className="my-8" />
<div>
<h5 className="text-xl font-semibold mb-4">Download PDF</h5>
<h5 className="text-xl font-semibold mb-4">
{t('modals.export.downloadPDF.heading')}
</h5>
<p className="leading-loose">
These options allow you to print a single page, unconstrained version
of your resume, perfect for those who have a lot of content.
Alternatively, you could download a multi-page version of your resume
as well with just one click.
</p>
<p className="leading-loose">{t('modals.export.downloadPDF.text')}</p>
<div className="mt-5 mb-4">
<div className="flex">
@ -108,14 +106,14 @@ const ExportModal = () => {
isLoading={isLoadingSingle}
onClick={handleSinglePageDownload}
>
Single Page Resume
{t('modals.export.downloadPDF.buttons.single')}
</Button>
<Button
className="ml-8"
isLoading={isLoadingMulti}
onClick={handleMultiPageDownload}
>
Multi Page Resume
{t('modals.export.downloadPDF.buttons.multi')}
</Button>
</div>
</div>
@ -124,18 +122,18 @@ const ExportModal = () => {
<hr className="my-8" />
<div>
<h5 className="text-xl font-semibold mb-4">Export to JSON Format</h5>
<h5 className="text-xl font-semibold mb-4">
{t('modals.export.jsonFormat.heading')}
</h5>
<p className="leading-loose">
You can also export your data into JSON format for safe keeping so
that you can easily import it back into Reactive Resume whenever you
want to edit or generate a resume.
</p>
<p className="leading-loose">{t('modals.export.jsonFormat.text')}</p>
<div className="mt-5">
<Button onClick={handleExportToJson}>Export JSON</Button>
<Button onClick={handleExportToJson}>
{t('modals.export.jsonFormat.button')}
</Button>
<a id="downloadAnchor" className="hidden">
Export JSON
{t('modals.export.jsonFormat.button')}
</a>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import * as Yup from 'yup';
import { useTranslation } from 'react-i18next';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils';
@ -10,11 +11,13 @@ const initialValues = {
name: '',
};
const schema = Yup.object().shape({
name: Yup.string().required('This is a required field.'),
});
const HobbyModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
name: Yup.string().required(t('shared.forms.validation.required')),
});
return (
<Formik
validateOnBlur
@ -23,13 +26,13 @@ const HobbyModal = () => {
>
{(formik) => (
<DataModal
name="Hobby"
name={t('builder.sections.hobby')}
path="hobbies.items"
event={ModalEvents.HOBBY_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Name"
label={t('shared.forms.name')}
placeholder="Fishing"
className="col-span-2"
{...getFieldProps(formik, schema, 'name')}

View File

@ -1,6 +1,7 @@
import { Tooltip } from '@material-ui/core';
import Ajv from 'ajv';
import React, { memo, useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import Button from '../../components/shared/Button';
import ModalContext from '../../contexts/ModalContext';
@ -10,6 +11,7 @@ import BaseModal from '../BaseModal';
const ImportModal = () => {
const ajv = new Ajv();
const { t } = useTranslation();
const fileInputRef = useRef(null);
const [open, setOpen] = useState(false);
const dispatch = useDispatch();
@ -38,21 +40,22 @@ const ImportModal = () => {
};
return (
<BaseModal hideActions state={[open, setOpen]} title="Import Your Resume">
<BaseModal
hideActions
state={[open, setOpen]}
title={t('builder.actions.import.heading')}
>
<div>
<h5 className="text-xl font-semibold mb-4">
Import from Reactive Resume
{t('modals.import.reactiveResume.heading')}
</h5>
<p>
Reactive Resume has it&apos;s own schema format to make the most of
all the customizable capabilities it has to offer. If you&apos;d like
to import a backup of your resume made with this app, just upload the
file using the button below.
<p className="leading-loose">
{t('modals.import.reactiveResume.text')}
</p>
<Button className="mt-5" onClick={() => fileInputRef.current.click()}>
Select File
{t('modals.import.button')}
</Button>
<input
ref={fileInputRef}
@ -65,18 +68,15 @@ const ImportModal = () => {
<hr className="my-8" />
<div>
<h5 className="text-xl font-semibold mb-4">Import from JSON Resume</h5>
<h5 className="text-xl font-semibold mb-4">
{t('modals.import.jsonResume.heading')}
</h5>
<p>
<a href="https://jsonresume.org/">JSON Resume</a> is an open standard
for resume schema structure. If you are one of the many enthusiasts
who have their resume ready in this format, all it takes it just one
click to get started with Reactive Resume.
</p>
<p className="leading-loose">{t('modals.import.jsonResume.text')}</p>
<Tooltip title="Coming Soon" placement="right" arrow>
<div className="mt-5 inline-block">
<Button className="opacity-50">Select File</Button>
<Button className="opacity-50">{t('modals.import.button')}</Button>
</div>
</Tooltip>
</div>
@ -84,16 +84,15 @@ const ImportModal = () => {
<hr className="my-8" />
<div>
<h5 className="text-xl font-semibold mb-4">Import from LinkedIn</h5>
<h5 className="text-xl font-semibold mb-4">
{t('modals.import.linkedIn.heading')}
</h5>
<p>
You can import a JSON that was exported from Reactive Resume by
clicking on the button below and selecting the appropriate file.
</p>
<p className="leading-loose">{t('modals.import.linkedIn.text')}</p>
<Tooltip title="Coming Soon" placement="right" arrow>
<div className="mt-5 inline-block">
<Button className="opacity-50">Select File</Button>
<Button className="opacity-50">{t('modals.import.button')}</Button>
</div>
</Tooltip>
</div>

View File

@ -1,5 +1,6 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
@ -11,12 +12,14 @@ const initialValues = {
fluency: '',
};
const schema = Yup.object().shape({
name: Yup.string().required('This is a required field.'),
fluency: Yup.string().required('This is a required field.'),
});
const LanguageModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
name: Yup.string().required(t('shared.forms.validation.required')),
fluency: Yup.string().required(t('shared.forms.validation.required')),
});
return (
<Formik
validateOnBlur
@ -25,19 +28,19 @@ const LanguageModal = () => {
>
{(formik) => (
<DataModal
name="Language"
name={t('builder.sections.language')}
path="languages.items"
event={ModalEvents.LANGUAGE_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Name"
label={t('shared.forms.name')}
placeholder="German"
{...getFieldProps(formik, schema, 'name')}
/>
<Input
label="Fluency"
label={t('builder.languages.fluency')}
placeholder="Native/B1"
{...getFieldProps(formik, schema, 'fluency')}
/>

View File

@ -1,6 +1,7 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import * as Yup from 'yup';
import { useTranslation } from 'react-i18next';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils';
@ -13,14 +14,16 @@ const initialValues = {
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 = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
title: Yup.string().required(t('shared.forms.validation.required')),
link: Yup.string().url(t('shared.forms.validation.url')),
date: Yup.date().max(new Date()),
summary: Yup.string(),
});
return (
<Formik
validateOnBlur
@ -29,33 +32,33 @@ const ProjectModal = () => {
>
{(formik) => (
<DataModal
name="Project"
name={t('builder.sections.project')}
path="projects.items"
event={ModalEvents.PROJECT_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Title"
label={t('shared.forms.title')}
className="col-span-2"
placeholder="Reactive Resume"
{...getFieldProps(formik, schema, 'title')}
/>
<Input
label="Link"
label={t('shared.forms.website')}
placeholder="https://github.com/AmruthPillai/Reactive-Resume"
{...getFieldProps(formik, schema, 'link')}
/>
<Input
type="date"
label="Date"
label={t('shared.forms.date')}
{...getFieldProps(formik, schema, 'date')}
/>
<Input
type="textarea"
label="Summary"
label={t('shared.forms.summary')}
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>

View File

@ -1,6 +1,7 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import * as Yup from 'yup';
import { useTranslation } from 'react-i18next';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils';
@ -14,15 +15,17 @@ const initialValues = {
summary: '',
};
const schema = Yup.object().shape({
name: Yup.string().required('This is a required field.'),
position: Yup.string().required('This is a required field.'),
phone: Yup.string(),
email: Yup.string().email('Must be a valid email address.'),
summary: Yup.string(),
});
const ReferenceModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
name: Yup.string().required(t('shared.forms.validation.required')),
position: Yup.string().required(t('shared.forms.validation.required')),
phone: Yup.string(),
email: Yup.string().email(t('shared.forms.validation.email')),
summary: Yup.string(),
});
return (
<Formik
validateOnBlur
@ -31,38 +34,38 @@ const ReferenceModal = () => {
>
{(formik) => (
<DataModal
name="Reference"
name={t('builder.sections.reference')}
path="references.items"
event={ModalEvents.REFERENCE_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Name"
label={t('shared.forms.name')}
placeholder="Jane Doe"
{...getFieldProps(formik, schema, 'name')}
/>
<Input
label="position"
label={t('shared.forms.position')}
placeholder="Assistant Manager"
{...getFieldProps(formik, schema, 'position')}
/>
<Input
label="Phone Number"
label={t('shared.forms.phone')}
placeholder="+1 (708) 756-6065"
{...getFieldProps(formik, schema, 'phone')}
/>
<Input
label="Email Address"
label={t('shared.forms.email')}
placeholder="janedoe@example.com"
{...getFieldProps(formik, schema, 'email')}
/>
<Input
type="textarea"
label="Summary"
label={t('shared.forms.summary')}
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>

View File

@ -1,5 +1,6 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
@ -19,14 +20,16 @@ const initialValues = {
level: SKILL_LEVELS[0],
};
const schema = Yup.object().shape({
name: Yup.string().required('This is a required field.'),
level: Yup.string()
.oneOf(SKILL_LEVELS, 'Must be one of the options above.')
.required('This is a required field.'),
});
const SkillModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
name: Yup.string().required(t('shared.forms.validation.required')),
level: Yup.string()
.oneOf(SKILL_LEVELS)
.required(t('shared.forms.validation.required')),
});
return (
<Formik
validateOnBlur
@ -35,19 +38,19 @@ const SkillModal = () => {
>
{(formik) => (
<DataModal
name="Skill"
name={t('builder.sections.skill')}
path="skills.items"
event={ModalEvents.SKILL_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Name"
label={t('shared.forms.name')}
placeholder="ReactJS"
{...getFieldProps(formik, schema, 'name')}
/>
<Input
label="Level"
label={t('builder.skills.level')}
type="dropdown"
options={SKILL_LEVELS}
{...getFieldProps(formik, schema, 'level')}

View File

@ -1,5 +1,6 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
@ -12,18 +13,20 @@ const initialValues = {
username: '',
};
const schema = Yup.object().shape({
network: Yup.string()
.min(5, 'Please enter at least 5 characters.')
.required('This is a required field.'),
username: Yup.string().required('This is a required field.'),
url: Yup.string()
.min(5, 'Please enter at least 5 characters.')
.required('This is a required field.')
.url('Must be a valid URL'),
});
const SocialModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
network: Yup.string()
.min(5, t('shared.forms.validation.min', { number: 5 }))
.required(t('shared.forms.validation.required')),
username: Yup.string().required(t('shared.forms.validation.required')),
url: Yup.string()
.min(5, t('shared.forms.validation.min', { number: 5 }))
.required(t('shared.forms.validation.required'))
.url(t('shared.forms.validation.url')),
});
return (
<Formik
validateOnBlur
@ -33,24 +36,24 @@ const SocialModal = () => {
{(formik) => (
<DataModal
path="social.items"
name="Social Network"
name={t('builder.sections.social')}
event={ModalEvents.SOCIAL_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Network"
label={t('builder.social.network')}
placeholder="Twitter"
{...getFieldProps(formik, schema, 'network')}
/>
<Input
label="Username"
label={t('builder.social.username')}
placeholder="KingOKings"
{...getFieldProps(formik, schema, 'username')}
/>
<Input
label="URL"
label={t('builder.social.url')}
className="col-span-2"
placeholder="https://twitter.com/KingOKings"
{...getFieldProps(formik, schema, 'url')}

View File

@ -1,6 +1,7 @@
import { Formik } from 'formik';
import React, { memo } from 'react';
import * as Yup from 'yup';
import { useTranslation } from 'react-i18next';
import Input from '../../components/shared/Input';
import ModalEvents from '../../constants/ModalEvents';
import { getFieldProps } from '../../utils';
@ -15,21 +16,26 @@ const initialValues = {
summary: '',
};
const schema = Yup.object().shape({
company: Yup.string().required('This is a required field.'),
position: Yup.string().required('This is a required field.'),
website: Yup.string().url('Must be a valid URL'),
startDate: Yup.date().required('This is a required field.'),
endDate: Yup.date().when(
'startDate',
(startDate, yupSchema) =>
startDate &&
yupSchema.min(startDate, 'End Date must be later than Start Date'),
),
summary: Yup.string().min(10, 'Please enter at least 10 characters.'),
});
const WorkModal = () => {
const { t } = useTranslation();
const schema = Yup.object().shape({
company: Yup.string().required(t('shared.forms.validation.required')),
position: Yup.string().required(t('shared.forms.validation.required')),
website: Yup.string().url(t('shared.forms.validation.url')),
startDate: Yup.date().required(t('shared.forms.validation.required')),
endDate: Yup.date().when(
'startDate',
(startDate, yupSchema) =>
startDate &&
yupSchema.min(startDate, t('shared.forms.validation.dateRange')),
),
summary: Yup.string().min(
10,
t('shared.forms.validation.min', { number: 10 }),
),
});
return (
<Formik
validateOnBlur
@ -39,46 +45,46 @@ const WorkModal = () => {
{(formik) => (
<DataModal
path="work.items"
name="Work Experience"
name={t('builder.sections.work')}
event={ModalEvents.WORK_MODAL}
>
<div className="grid grid-cols-2 gap-8">
<Input
label="Company"
label={t('builder.work.company')}
className="col-span-2"
placeholder="Postdot Technologies Pvt. Ltd."
{...getFieldProps(formik, schema, 'company')}
/>
<Input
label="Position"
label={t('shared.forms.position')}
placeholder="Full Stack Web Developer"
{...getFieldProps(formik, schema, 'position')}
/>
<Input
label="Website"
label={t('shared.forms.website')}
placeholder="https://example.com/"
{...getFieldProps(formik, schema, 'website')}
/>
<Input
type="date"
label="Start Date"
label={t('shared.forms.startDate')}
placeholder="6th August 208"
{...getFieldProps(formik, schema, 'startDate')}
/>
<Input
type="date"
label="End Date"
label={t('shared.forms.endDate')}
placeholder="6th August 208"
{...getFieldProps(formik, schema, 'endDate')}
/>
<Input
type="textarea"
label="Summary"
label={t('shared.forms.summary')}
className="col-span-2"
{...getFieldProps(formik, schema, 'summary')}
/>

View File

@ -1,6 +1,7 @@
import { navigate } from 'gatsby';
import React, { memo, useContext, useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import Artboard from '../../components/builder/center/Artboard';
import LeftSidebar from '../../components/builder/left/LeftSidebar';
import RightSidebar from '../../components/builder/right/RightSidebar';
@ -11,6 +12,7 @@ import Button from '../../components/shared/Button';
const Builder = ({ id }) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const { getResume } = useContext(DatabaseContext);
@ -24,22 +26,17 @@ const Builder = ({ id }) => {
if (!resume) {
navigate('/app/dashboard');
toast.error(
`The resume you were looking for does not exist anymore... or maybe it never did?`,
);
toast.error(t('builder.toasts.doesNotExist'));
return null;
}
if (resume.createdAt === resume.updatedAt) {
toast.dark(() => (
<div className="py-2">
<p className="leading-loose">
Not sure where to begin? Try <strong>Loading Demo Data</strong> to
see what Reactive Resume has to offer.
</p>
<p className="leading-loose">{t('builder.toasts.loadDemoData')}</p>
<Button className="mt-4" onClick={handleLoadDemoData}>
Load Demo Data
{t('builder.actions.loadDemoData.button')}
</Button>
</div>
));
@ -60,7 +57,7 @@ const Builder = ({ id }) => {
<div className="col-span-3">
<LeftSidebar />
</div>
<div className="h-screen overflow-scroll col-span-5 bg-primary-100 grid items-center justify-center">
<div className="col-span-5 h-screen overflow-hidden bg-primary-100 grid items-center justify-center">
<Artboard />
</div>
<div className="col-span-3">

View File

@ -1,12 +1,14 @@
import firebase from 'gatsby-plugin-firebase';
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
import CreateResume from '../../components/dashboard/CreateResume';
import ResumePreview from '../../components/dashboard/ResumePreview';
import TopNavbar from '../../components/dashboard/TopNavbar';
import LoadingScreen from '../../components/router/LoadingScreen';
const Dashboard = ({ user }) => {
const { t } = useTranslation();
const [resumes, setResumes] = useState([]);
const [loading, setLoading] = useState(true);
@ -61,7 +63,9 @@ const Dashboard = ({ user }) => {
return (
<div>
<Helmet>
<title>Dashboard | Reactive Resume</title>
<title>
{t('dashboard.title')} | {t('shared.appName')}
</title>
<link rel="canonical" href="https://rxresu.me/app/dashboard" />
</Helmet>

View File

@ -1,11 +1,17 @@
import React from 'react';
import { MdKeyboardArrowLeft } from 'react-icons/md';
import { Link } from '@reach/router';
import React from 'react';
import { Helmet } from 'react-helmet';
import { MdKeyboardArrowLeft } from 'react-icons/md';
import Wrapper from '../components/shared/Wrapper';
const FrequentlyAskedQuestions = () => {
return (
<Wrapper>
<Helmet>
<title>Frequently Asked Questions | Reactive Resume</title>
<link rel="canonical" href="https://rxresu.me/app/dashboard" />
</Helmet>
<div className="md:w-1/2 container px-8 md:px-0 py-16 grid gap-12">
<div className="flex items-center">
<Link to="/">

View File

@ -1,24 +1,23 @@
import React, { memo } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from '@reach/router';
import { useTranslation } from 'react-i18next';
import Hero from '../components/landing/Hero';
import Wrapper from '../components/shared/Wrapper';
const Home = () => {
const { t } = useTranslation();
return (
<Wrapper>
<Helmet>
<title>Reactive Resume</title>
<title>{t('shared.appName')}</title>
<link rel="canonical" href="https://rxresu.me/" />
</Helmet>
<div className="container mt-24">
<Hero />
<div className="my-24">
<h4 className="text-xl uppercase font-bold mb-8">Screenshots</h4>
</div>
<div className="pt-8">
<Feature title="Create a resume thats worthy of who you are.">
Keep up with the latest trends in resume design without having to
@ -37,19 +36,12 @@ const Home = () => {
</Feature>
<Feature title="Kickstarting your career shouldnt come at a cost.">
There are brilliant alternatives to this app like{' '}
<a href="/" target="blank">
Novoresume
</a>{' '}
and{' '}
<a href="/" target="blank">
Zety
</a>
, but they come at a cost, mainly because of the time the developers
and the marketing they had to incur to make the product. This app
might not be better than them, but it does cater to people who are
just not in a position to pay hundreds of dollars to create a resume
to bootstrap their career.
There are brilliant alternatives to this app like Novoresume and
Zety , but they come at a cost, mainly because of the time the
developers and the marketing they had to incur to make the product.
This app might not be better than them, but it does cater to people
who are just not in a position to pay hundreds of dollars to create
a resume to bootstrap their career.
</Feature>
<Feature title="Your data is your data, none of my data.">
@ -69,11 +61,29 @@ const Home = () => {
</h4>
<div className="grid grid-cols-4 gap-8">
<Link to="/faq">Frequently Asked Questions</Link>
<Link to="/faq">Checkout Source Code</Link>
<a
href="https://github.com/AmruthPillai/Reactive-Resume"
rel="noreferrer"
target="_blank"
>
Check Out Source Code
</a>
<Link to="/faq">Upvote on Product Hunt</Link>
<Link to="/faq">Raise an Issue on GitHub</Link>
<Link to="/faq">Donate to Reactive Resume</Link>
<Link to="/faq">Building Great Looking Resumes</Link>
<a
href="https://www.buymeacoffee.com/AmruthPillai"
rel="noreferrer"
target="_blank"
>
Donate to Reactive Resume
</a>
<Link to="/blog/design-beautiful-resumes">
Design Beautiful Resumes
</Link>
<Link to="/blog/ats-friendly-resumes">ATS-Friendly Resumes</Link>
<Link to="/blog/acing-video-interviews">
Acing Video Interviews
</Link>
<Link to="/blog/jobs-during-covid-19">Jobs During COVID-19</Link>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { Link, navigate } from '@reach/router';
import React, { memo, useContext, useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import LoadingScreen from '../../components/router/LoadingScreen';
import DatabaseContext from '../../contexts/DatabaseContext';
import Castform from '../../templates/Castform';
@ -13,6 +14,7 @@ import styles from './view.module.css';
import Celebi from '../../templates/Celebi';
const ResumeViewer = ({ id }) => {
const { t } = useTranslation();
const [resume, setResume] = useState(null);
const [loading, setLoading] = useState(true);
const { getResume } = useContext(DatabaseContext);
@ -42,7 +44,9 @@ const ResumeViewer = ({ id }) => {
return (
<div className={styles.container}>
<Helmet>
<title>{resume.name} | Reactive Resume</title>
<title>
{resume.name} | {t('shared.appName')}
</title>
<link rel="canonical" href={`https://rxresu.me/r/${id}`} />
</Helmet>

View File

@ -1,4 +1,5 @@
import React, { memo, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
@ -18,13 +19,16 @@ const ContactItem = ({ value, label, link }) => {
};
const ContactC = () => {
const { t } = useTranslation();
const { data } = useContext(PageContext);
return (
<div className="text-xs grid gap-2">
{data.profile.address.line1 && (
<div>
<h6 className="capitalize font-semibold">Address</h6>
<h6 className="capitalize font-semibold">
{t('shared.forms.address')}
</h6>
<div className="flex flex-col text-xs">
<span>{data.profile.address.line1}</span>
<span>{data.profile.address.line2}</span>

View File

@ -1,4 +1,5 @@
import React, { memo, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { MdFlare } from 'react-icons/md';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
@ -19,6 +20,7 @@ const ContactItem = ({ value, label, link }) => {
};
const ContactD = () => {
const { t } = useTranslation();
const { data } = useContext(PageContext);
return (
@ -42,7 +44,9 @@ const ContactD = () => {
{data.profile.address.line1 && (
<div>
<h6 className="capitalize font-semibold">Address</h6>
<h6 className="capitalize font-semibold">
{t('shared.forms.address')}
</h6>
<div className="flex flex-col text-xs">
<span>{data.profile.address.line1}</span>
<span>{data.profile.address.line2}</span>

View File

@ -1,4 +1,5 @@
import React, { memo, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
@ -18,14 +19,17 @@ const ContactItem = ({ value, label, link }) => {
};
const ContactE = () => {
const { t } = useTranslation();
const { data, heading: Heading } = useContext(PageContext);
return (
<div>
<Heading>Profile</Heading>
<Heading>{t('builder.sections.profile')}</Heading>
<div className="relative w-full grid gap-2 text-xs">
<div>
<h6 className="capitalize font-semibold">Address</h6>
<h6 className="capitalize font-semibold">
{t('shared.forms.address')}
</h6>
<div className="flex flex-col text-xs">
<span>{data.profile.address.line1}</span>
<span>{data.profile.address.line2}</span>

View File

@ -1,8 +1,12 @@
import { get, isEmpty } from 'lodash';
import moment from 'moment';
import { useTranslation } from 'react-i18next';
export const getModalText = (isEditMode, type) => {
return isEditMode ? `Edit ${type}` : `Add ${type}`;
const { t } = useTranslation();
return isEditMode
? `${t('shared.buttons.edit')} ${type}`
: `${t('shared.buttons.add')} ${type}`;
};
export const safetyCheck = (section, path = 'items') => {
@ -30,6 +34,11 @@ export const getFieldProps = (formik, schema, name) => ({
...formik.getFieldProps(name),
});
export const getUnsplashPhoto = async () => {
const response = await fetch('https://source.unsplash.com/featured/400x600');
return response.url;
};
export const hexToRgb = (hex) => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);