Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30d0151bdb | |||
| 4dd94c3363 | |||
| f711b089bc | |||
| 01c1125153 | |||
| fa42d82416 | |||
| 6322d4d105 | |||
| 77467929c7 | |||
| 3a524f9c9c | |||
| 63f900870b | |||
| 32f78e85f7 | |||
| 1cc2232730 | |||
| 2ff6761630 | |||
| 5836e55a36 | |||
| ec98c14fbd | |||
| 78c1f5a380 | |||
| 808fa45124 | |||
| 2625ed4f3d | |||
| 40085f8d78 | |||
| f4e3be178c | |||
| 601f61c59a | |||
| 59049e8f77 | |||
| 798e77f693 | |||
| 78565079e7 | |||
| 0bec4cff05 | |||
| 26dc0069f9 | |||
| 90bb80b1e2 | |||
| 61ed3ff018 | |||
| 36a12e82a2 | |||
| a3cf1752cc | |||
| 5b79e23564 | |||
| 300e4a790d | |||
| ba4666b767 | |||
| b283c6ee8f | |||
| 316eca35ef | |||
| 16c18de964 | |||
| 0c23af4be8 | |||
| fe1b325fdf | |||
| 9395a4d578 | |||
| c11f92841b | |||
| 2654cba039 | |||
| 7d8828a358 | |||
| 8bc7d2599e | |||
| 036adbfc96 | |||
| 4b7e43424c | |||
| 0f1c3a8142 | |||
| 8dc27ecf07 | |||
| a05917b00d | |||
| d5f2eea34c | |||
| 29bc3f33a6 | |||
| b332b77eff | |||
| 8e09db276e | |||
| 2f7cfd2add | |||
| 6a4464b239 | |||
| 972e8b1bcf | |||
| ad916c5b07 | |||
| eca80a1663 | |||
| 8f48f5fcd6 | |||
| 40f5111eba | |||
| 55a09c0c05 | |||
| 1e72efa7ac | |||
| fd752bfd70 | |||
| ee328186c8 | |||
| ecab1e0bfa | |||
| cbbdc92c66 | |||
| 5d54f8101b | |||
| 4fe5788b23 | |||
| 612335696c | |||
| 781dc4d231 | |||
| 5d37dcb0ed | |||
| 6255849822 | |||
| ef3b2c5638 | |||
| 6c671f2dba | |||
| 4447b58b8f | |||
| b210b19b03 | |||
| 1e909f3257 | |||
| c3f037ee1d | |||
| 6f02048ebd | |||
| 78cd1c036e | |||
| 7e1448bbf9 | |||
| dc4aa0b496 | |||
| a131bb3652 | |||
| bf9da32465 | |||
| efba6ee7dd | |||
| 9c2ff5e14f | |||
| d167baa607 | |||
| e420ea6ce4 | |||
| ec0a88675b | |||
| 11827dcc29 | |||
| 1f6f052129 | |||
| 66b98a21e8 | |||
| b017d7aa41 | |||
| 62398fd96f | |||
| 8167f51ce1 | |||
| f0a381a37c | |||
| d923dfe3c0 | |||
| d3f1cc746a | |||
| 8ee4993321 | |||
| 917850157e | |||
| b4856be5ab | |||
| fc29fb8eb4 | |||
| 2e288a5407 | |||
| 43ce43ab5b | |||
| 5a2594eb88 | |||
| e52edaa552 | |||
| 54fd97b5ec | |||
| 9df12194bf | |||
| e96b090904 | |||
| d79997d380 | |||
| 2696a54d17 | |||
| 28ba2b1b8f | |||
| deb51f0e29 | |||
| aa5e748cca | |||
| b0a295d8bb | |||
| c738f311da | |||
| cff51a8be9 | |||
| 1a0ab6fb22 | |||
| 938e2e8e25 | |||
| 9c1380f401 | |||
| 00505a9e5d | |||
| 37f0ab3bca | |||
| a4983ac6bc | |||
| 7c73685759 | |||
| 269d5206e6 | |||
| e1529e03f9 | |||
| 5104ea6438 | |||
| 36f41c2f9d | |||
| 7e50c8e85b | |||
| 993fd82f4c | |||
| ed6578b052 | |||
| 5fc7a32c67 | |||
| 58160b2b6e | |||
| 2aa3786f5f | |||
| a30011e841 | |||
| 295172687b | |||
| 2175256310 | |||
| ff892e3ea5 | |||
| 0e2e50d658 | |||
| 4657f1f5f5 | |||
| 81fc3b981c | |||
| e961d043da | |||
| 4d4c24b79d | |||
| d40539680a | |||
| ea0a5ec989 | |||
| 9e855be82c | |||
| a9be89a754 | |||
| 789c49e47e | |||
| 56f46a185c | |||
| 3a8f4f00d0 | |||
| e9c7f33d30 | |||
| 7e1483b2a2 | |||
| d3359cfa58 | |||
| 04afc009ef | |||
| 46090b0793 | |||
| b617ec6bfa | |||
| fd15989346 | |||
| fd00a4b4e1 | |||
| 30d567d853 | |||
| a9d1f0fa7b | |||
| 3be316e9dd | |||
| 2426144d1b | |||
| c04367b7b5 | |||
| 59828140ee | |||
| 8b12d366f9 |
@ -1,4 +1,21 @@
|
||||
.cache/
|
||||
node_modules/
|
||||
functions/
|
||||
public/
|
||||
# Build Artifacts
|
||||
dist
|
||||
.next
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
|
||||
# Project Metadata
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Android App
|
||||
/app
|
||||
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
38
.env.example
@ -1,8 +1,30 @@
|
||||
FIREBASE_APIKEY=""
|
||||
FIREBASE_APPID=""
|
||||
FIREBASE_AUTHDOMAIN=""
|
||||
FIREBASE_DATABASEURL=""
|
||||
FIREBASE_MEASUREMENTID=""
|
||||
FIREBASE_MESSAGINGSENDERID=""
|
||||
FIREBASE_PROJECTID=""
|
||||
FIREBASE_STORAGEBUCKET=""
|
||||
# App
|
||||
TZ=UTC
|
||||
SECRET_KEY=change-me
|
||||
|
||||
# URLs
|
||||
PUBLIC_URL=http://localhost:3000
|
||||
PUBLIC_SERVER_URL=http://localhost:3100
|
||||
|
||||
# Database
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DATABASE=reactive_resume
|
||||
POSTGRES_SSL_CERT=
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=change-me
|
||||
JWT_EXPIRY_TIME=604800
|
||||
|
||||
# Google
|
||||
PUBLIC_GOOGLE_CLIENT_ID=change-me
|
||||
GOOGLE_CLIENT_SECRET=change-me
|
||||
GOOGLE_API_KEY=change-me
|
||||
|
||||
# SendGrid (Optional)
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
||||
SENDGRID_FROM_NAME=
|
||||
SENDGRID_FROM_EMAIL=
|
||||
@ -1,4 +0,0 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
36
.eslintrc
@ -1,36 +0,0 @@
|
||||
{
|
||||
"globals": {
|
||||
"atob": true,
|
||||
"Blob": true,
|
||||
"fetch": true,
|
||||
"window": true,
|
||||
"document": true,
|
||||
"FileReader": true,
|
||||
"localStorage": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest/style",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["jest", "prettier", "sort-imports-es6-autofix"],
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
|
||||
"jsx-a11y/label-has-associated-control": 0,
|
||||
"react/jsx-one-expression-per-line": 0,
|
||||
"react/jsx-props-no-spreading": 0,
|
||||
"prettier/prettier": ["error"],
|
||||
"react/no-array-index-key": 0,
|
||||
"jsx-a11y/anchor-is-valid": 0,
|
||||
"react/button-has-type": 0,
|
||||
"no-unused-expressions": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-param-reassign": 0,
|
||||
"consistent-return": 0,
|
||||
"no-nested-ternary": 0,
|
||||
"react/prop-types": 0,
|
||||
"no-plusplus": 0
|
||||
}
|
||||
}
|
||||
38
.eslintrc.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": ["/app"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"],
|
||||
"rules": {
|
||||
// TypeScript ESLint
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
// Simple Import Sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
// Unused Imports
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "none",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
.firebaserc
@ -1,14 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "rx-resume"
|
||||
},
|
||||
"targets": {
|
||||
"rx-resume": {
|
||||
"hosting": {
|
||||
"rxresume": [
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.github/FUNDING.yml
vendored
@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
custom: ['buymeacoffee.com/AmruthPillai']
|
||||
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help improve
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
16
.github/workflows/close-stale.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v5.0.0
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove the stale label or comment on this PR, otherwise it would be closed in 5 days.'
|
||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove the stale label or comment on this issue, otherwise it would be closed in 5 days.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
34
.github/workflows/codeql-analysis.yml
vendored
@ -1,34 +0,0 @@
|
||||
name: CodeQL Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
schedule:
|
||||
- cron: '37 16 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
21
.github/workflows/digitalocean-deploy.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: Deploy Latest Version on DigitalOcean
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Build and Push Docker Image
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install DigitalOcean CLI
|
||||
uses: digitalocean/action-doctl@v2.1.0
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||
|
||||
- name: Create Deployment with Latest Version
|
||||
run: doctl apps create-deployment ${{ secrets.DIGITALOCEAN_APP_ID }} --wait --force-rebuild
|
||||
120
.github/workflows/docker-build-push.yml
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
docker_client:
|
||||
name: Docker (Client)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and Push Client Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: client/Dockerfile
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
|
||||
docker_server:
|
||||
name: Docker (Server)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and Push Server Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: server/Dockerfile
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||
|
||||
github_client:
|
||||
name: GitHub (Client)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Client Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: client/Dockerfile
|
||||
tags: |
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
|
||||
github_server:
|
||||
name: GitHub (Server)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Server Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: server/Dockerfile
|
||||
tags: |
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||
27
.github/workflows/run-tests.yml
vendored
@ -1,27 +0,0 @@
|
||||
name: Run Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
80
.gitignore
vendored
@ -1,76 +1,10 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Coverage directory used by Jest
|
||||
test-coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variable files
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
|
||||
# Firebase Files
|
||||
.firebase
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
.pnp/
|
||||
.pnp.js
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
# macOS
|
||||
.DS_Store
|
||||
1
.husky/.gitignore
vendored
@ -1 +0,0 @@
|
||||
_
|
||||
@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
pnpm install
|
||||
pnpm run lint
|
||||
pnpm run format
|
||||
5
.idea/.gitignore
generated
vendored
@ -1,5 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
12
.idea/Reactive-Resume.iml
generated
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
58
.idea/codeStyles/Project.xml
generated
@ -1,58 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
7
.idea/inspectionProfiles/Project_Default.xml
generated
@ -1,7 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Reactive-Resume.iml" filepath="$PROJECT_DIR$/.idea/Reactive-Resume.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,4 +1,32 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
# Schema
|
||||
schema/dist
|
||||
|
||||
# Server
|
||||
server/dist
|
||||
|
||||
# Client
|
||||
client/.next
|
||||
client/public/__ENV.js
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
|
||||
# Project Metadata
|
||||
LICENSE
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Android App
|
||||
/app
|
||||
|
||||
# Docs
|
||||
docs/build
|
||||
docs/.docusaurus
|
||||
@ -1,5 +1,4 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"printWidth": 120,
|
||||
"singleQuote": true
|
||||
}
|
||||
|
||||
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Debug: Server",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"stopOnEntry": false,
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"name": "Debug: Client",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm run dev:client",
|
||||
"console": "integratedTerminal",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
28
.vscode/settings.json
vendored
@ -1,11 +1,25 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.js": "javascriptreact"
|
||||
},
|
||||
"css.validate": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/i18n/locales"],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.wordWrap": "on",
|
||||
"eslint.workingDirectories": [
|
||||
"schema",
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"react"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"css.validate": false
|
||||
}
|
||||
"i18n-ally.localesPaths": [
|
||||
"client/public/locales"
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"scss.validate": false
|
||||
}
|
||||
28
CHANGELOG.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [3.0.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0-beta.6...v3.0.0) (2022-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang**: add German, Kannada and Tamil languages to the app ([3a524f9](https://github.com/AmruthPillai/Reactive-Resume/commit/3a524f9c9c7a0e446491265b2242ad3dfeae188c))
|
||||
* **docs:** add docusaurus workspace, initial setup of docs ([dc4aa0b](https://github.com/AmruthPillai/Reactive-Resume/commit/dc4aa0b496096bd59c45426bfcea6ba7db5f5c01))
|
||||
|
||||
## [3.0.0-beta.6](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2022-03-11)
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** add language switcher on the landing page, in the footer ([8bc7d25](https://github.com/AmruthPillai/Reactive-Resume/commit/8bc7d2599ef6af7a07bfbe886c43844152b0d9f7))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** add missing translation keys, update lang/locale logic ([7d8828a](https://github.com/AmruthPillai/Reactive-Resume/commit/7d8828a358d653bb162877a64c75028eb82678cd))
|
||||
* **webkit:** fix issue with webkit not supporting .at() ([2654cba](https://github.com/AmruthPillai/Reactive-Resume/commit/2654cba039eb73d33257c36fa90a52cabc9fda96))
|
||||
|
||||
## [3.0.0-beta.5](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2022-03-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** fix issue with using swipelayout ([972e8b1](https://github.com/AmruthPillai/Reactive-Resume/commit/972e8b1bcf9ad44d8915bf23d189711672937bc0))
|
||||
@ -1,65 +0,0 @@
|
||||
# Deployment
|
||||
|
||||
This is a guide on how to build the source from scratch, along with setting up Firebase and related cloud functions to be able to export PDFs just like the original deployment of [rxresu.me](http://rxresu.me/).
|
||||
|
||||
### Requirements
|
||||
|
||||
- A Firebase project
|
||||
- Works on both Linux, macOS and Windows
|
||||
- Requires Node.js & NPM installed on the machine
|
||||
|
||||
### Setting up Firebase
|
||||
|
||||
1. Create a new Firebase project by visiting [Firebase Console](https://console.firebase.google.com/) and clicking on `Add Project`
|
||||
|
||||

|
||||
|
||||
2. Disable Google Analytics, or keep it enabled as per your requirements. Most people wouldn't need it.
|
||||
|
||||

|
||||
|
||||
3. Wait until Project is created, then click on Continue
|
||||
|
||||

|
||||
|
||||
4. Navigate to Realtime Database, and click on `Create Database`
|
||||
|
||||

|
||||
|
||||
5. Select any location that's nearby to you, and most importantly, create the database in `Test Mode` and click on Enable
|
||||
|
||||

|
||||
|
||||
6. Go back to Project Overview and click on `Web` and skip through every other step by clicking `Next`.
|
||||
|
||||

|
||||
|
||||
7. Copy configuration variables of your project, or keep this page open as you will need it later
|
||||
|
||||

|
||||
|
||||
### Cloning the Repository
|
||||
|
||||
1. Run this command on your machine's terminal or Command Prompt
|
||||
|
||||
```
|
||||
git clone git@github.com:AmruthPillai/Reactive-Resume.git
|
||||
```
|
||||
|
||||
<img width="550" alt="Screenshot 2021-03-13 at 10 38 16 AM" src="https://user-images.githubusercontent.com/1134738/111019919-3df43d00-83e8-11eb-8d6b-d9fe0cc74a3a.png">
|
||||
|
||||
2. Copy the file `.env.example` to `.env` and start editing the file
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
<img width="317" alt="Screenshot 2021-03-13 at 10 50 21 AM" src="https://user-images.githubusercontent.com/1134738/111020166-ed7ddf00-83e9-11eb-9cbb-a8732243bbd5.png">
|
||||
|
||||
3. Copy configuration variables from last step to the .env file, it's fine to have `FIREBASE_MEASUREMENTID` empty if you had Google Analytics disabled.
|
||||
|
||||
<img width="696" alt="Screenshot 2021-03-13 at 10 51 53 AM" src="https://user-images.githubusercontent.com/1134738/111020217-3c2b7900-83ea-11eb-801d-d8719cf23608.png">
|
||||
|
||||
4. Run `npm install` on the terminal/command prompt
|
||||
|
||||
5. After that's done, run `npm run build` and allow some time for the process to build
|
||||
38
Dockerfile
@ -1,38 +0,0 @@
|
||||
FROM node:alpine as builder
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
g++ \
|
||||
yasm \
|
||||
bash \
|
||||
make \
|
||||
automake \
|
||||
autoconf \
|
||||
libtool \
|
||||
zlib-dev \
|
||||
libpng-dev
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
--repository http://dl-3.alpinelinux.org/alpine/edge/community \
|
||||
--repository http://dl-3.alpinelinux.org/alpine/edge/main \
|
||||
vips-dev
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
ARG FIREBASE_APIKEY
|
||||
ARG FIREBASE_APPID
|
||||
ARG FIREBASE_AUTHDOMAIN
|
||||
ARG FIREBASE_DATABASEURL
|
||||
ARG FIREBASE_MEASUREMENTID
|
||||
ARG FIREBASE_MESSAGINGSENDERID
|
||||
ARG FIREBASE_PROJECTID
|
||||
ARG FIREBASE_STORAGEBUCKET
|
||||
|
||||
COPY . ./
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
RUN rm -rf /usr/share/nginx/html
|
||||
COPY --from=builder /app/public/ /usr/share/nginx/html
|
||||
COPY server.conf /etc/nginx/conf.d/default.conf
|
||||
4
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Amruth Pillai
|
||||
Copyright (c) 2022 Amruth Pillai
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
159
README.md
@ -1,138 +1,95 @@
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/logo.png" width="256px" />
|
||||
<img src="https://i.imgur.com/pc8Ingg.png" alt="Reactive Resume" width="256px" height="256px" />
|
||||
|
||||
## A free and open source resume builder.
|
||||
# Reactive Resume
|
||||
|
||||
[](https://crowdin.com/project/reactive-resume)
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/blob/develop/LICENSE)
|
||||

|
||||

|
||||
[](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
||||
|
||||
### [Go to App](https://rxresu.me/)
|
||||
|
||||
### What is this app all about?
|
||||
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
||||
|
||||
Reactive Resume is a free and open source resume builder that’s built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3. With this app, you can create multiple resumes, share them with recruiters through a unique link and print as PDF, all for free, no advertisements, without losing the integrity and privacy of your data.
|
||||
|
||||
You have complete control over what goes into your resume, how it looks, what colors, what templates, even the layout in which sections placed. Want a dark mode resume? It’s as easy as editing 3 values and you’re done. You don’t need to wait to see your changes either. Everything you type, everything you change, appears immediately on your resume and gets updated in real time.
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
- Manage multiple resumes with one account
|
||||
- Free, forever
|
||||
- No Advertising
|
||||
- No User Tracking
|
||||
- Sync your data across devices
|
||||
- Sign in with Google, or sign in anonymously just to test the app
|
||||
- Accessible in multiple languages
|
||||
- Import data from [LinkedIn](https://www.linkedin.com/), [JSON Resume](https://jsonresume.org/)
|
||||
- Manage multiple resumes with one account
|
||||
- Open Source (with large community support)
|
||||
- Send your resume to others with a unique sharable link
|
||||
- Pick any font from [Google Fonts](https://fonts.google.com/) to use on your resume
|
||||
- Choose from 6 vibrant templates and more coming soon
|
||||
- Structure sections and change layouts the way you want to
|
||||
- Rename sections according to your language/industry
|
||||
- Export your resume to JSON or PDF format with just one click
|
||||
- Create an account using your email, or just Sign in with Google
|
||||
- Mix and match colors to any degree, even a dark mode resume?
|
||||
- Pick from a variety of crisp and clear fonts
|
||||
- Easy to translate to your own language
|
||||
- Import your existing [JSON Resume](https://jsonresume.org/) in one click
|
||||
- No advertisements, no data sharing, no marketing emails
|
||||
- **Everything is free, and there’s no catch!**
|
||||
- Add sections, add pages and change layouts the way you want to
|
||||
- Tailor-made Backend and Database, isolated from Google, Amazon etc.
|
||||
- **Oh, and did I mention that it's free?**
|
||||
|
||||
### Screenshots
|
||||
## Languages
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/screenshots/screen-1.png" width="400px" />
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/screenshots/screen-3.png" width="400px" />
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/screenshots/screen-5.png" width="400px" />
|
||||
|
||||
### Translation
|
||||
|
||||
To translate the app, just fork the repository, go to `src/i18n/locales` and duplicate the `en.json` file to a new file `your-lang-code.json` and translate all of the strings inside. It's a simple process that would take just a few minutes, and by contributing, your name could also be added down below as a contributor.
|
||||
|
||||
For those of you familiar with the Crowdin Platform, you could do that too and just head to http://crowdin.com/project/reactive-resume/ to translate the app over there. They have a great interface that helps you navigate through various strings and manage translations.
|
||||
|
||||
##### Languages Currently Supported
|
||||
|
||||
- Arabic (عربى)
|
||||
- Bengali (বাংলা)
|
||||
- Czech (čeština)
|
||||
- Chinese Simplified (简体中文)
|
||||
- Danish (Dansk)
|
||||
- Dutch (Nederlands)
|
||||
- English (US)
|
||||
- Finnish (Suomalainen)
|
||||
- French (Français)
|
||||
- English
|
||||
- German (Deutsch)
|
||||
- Greek (Ελληνικά)
|
||||
- Hebrew (עִברִית)
|
||||
- Hindi (हिंदी)
|
||||
- Indonesian (Bahasa Indonesia)
|
||||
- Italian (Italiano)
|
||||
- Japanese (日本人)
|
||||
- Kannada (ಕನ್ನಡ)
|
||||
- Lithuanian (Lietuvių)
|
||||
- Norwegian (Norsk)
|
||||
- Persian (Farsi)
|
||||
- Polish (Polskie)
|
||||
- Portuguese (Brazilian)
|
||||
- Portuguese (Portugal)
|
||||
- Romanian (Română)
|
||||
- Russian (русский)
|
||||
- Slovak (Slovenčina)
|
||||
- Spanish (Español)
|
||||
- Swedish (Svenska)
|
||||
- Turkish (Türkçe)
|
||||
- Ukrainian (Українська)
|
||||
- Hindi (हिन्दी)
|
||||
- Kannada (ಕನ್ನಡ) (@aksh1251)
|
||||
- Tamil (தமிழ்)
|
||||
|
||||
Thank you to all the amazing people who have contributed to Reactive Resume by translating it into their native language.
|
||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||
|
||||
### Building from Source
|
||||
## Tutorial
|
||||
|
||||
Want to run your own instance of Reactive Resume? You are very much free to do so. The requirements to build from source are:
|
||||
The docs include an extensive [Tutorial](https://docs.rxresu.me/tutorial) section which outline the features of Reactive Resume and help you through building your first resume on the app.
|
||||
|
||||
- NodeJS/NPM
|
||||
- A Firebase Project
|
||||
## Build from Source
|
||||
|
||||
1. First, clone this project repository
|
||||
For extensive information on how to build the app on your local machine, head over to the docs's [Source Code](https://docs.rxresu.me/source-code) section.
|
||||
|
||||
```
|
||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
||||
cd Reactive-Resume
|
||||
```
|
||||
## Contributing
|
||||
|
||||
2. Run npm install to install dependencies for the project
|
||||
This project makes use of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) style and workflow for commit messages to ensure that the CHANGELOG is auto-generated. In general, this project follows the "fork-and-pull" Git workflow.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
1. **Fork** the repo on GitHub
|
||||
2. **Clone** the project to your own machine
|
||||
3. **Commit** changes to your own branch
|
||||
4. **Push** your work back up to your fork
|
||||
5. Submit a **Pull Request** so that we can review your changes
|
||||
|
||||
3. Create a `.env` file and fill it with your Firebase credentials
|
||||
NOTE: Be sure to merge the latest from `main` before making a pull request!
|
||||
|
||||
You can get these by setting up a firebase web app [here](https://console.firebase.google.com/u/0/).
|
||||
## Bugs? Feature Requests?
|
||||
|
||||
Also note that you'll need to set up a Realtime Database, _not_ a Firestore Database, to get the correct value for `FIREBASE_DATABASEURL`. Be sure to set it to test mode so you can read/write data. Just remember to either revert these or remove the database after your testing is completed.
|
||||
Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose) platform to notify me about bugs or new features that you would like to see in Reactive Resume. Please check before creating new issues as there might already be one.
|
||||
|
||||
```
|
||||
FIREBASE_APIKEY=""
|
||||
FIREBASE_APPID=""
|
||||
FIREBASE_AUTHDOMAIN=""
|
||||
FIREBASE_DATABASEURL=""
|
||||
FIREBASE_MEASUREMENTID=""
|
||||
FIREBASE_MESSAGINGSENDERID=""
|
||||
FIREBASE_PROJECTID=""
|
||||
FIREBASE_STORAGEBUCKET=""
|
||||
```
|
||||
## Donations
|
||||
|
||||
4. Run `npm run start` to run the development server or `npm run build` to build the production app.
|
||||
Reactive Resume would be nothing without the folks who supported me and kept the project alive in the beginning, and your cotinued support is what keeps me going. If you found Reactive Resume to be useful, helpful or just insightful and appreciate the effort I took to make the project, please consider donating as little or as much as your can.
|
||||
|
||||
And that's it! 🎉
|
||||
### [☕️ Buy me a coffee](https://www.buymeacoffee.com/AmruthPillai) | [💸 PayPal](https://paypal.me/RajaRajanA)
|
||||
|
||||
### Donation
|
||||
## Infrastructure
|
||||
|
||||
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, please consider donating as little as \$5 to help keep the project alive :)
|
||||
- [Next.js](https://nextjs.org/), frontend
|
||||
- [NestJS](https://nestjs.com/), backend
|
||||
- [PostgreSQL](https://www.postgresql.org/), database
|
||||
- [DigitalOcean](https://www.digitalocean.com/), infrastructure provider
|
||||
- [Crowdin](https://translate.rxresu.me/), translation management platform
|
||||
|
||||
#### https://www.buymeacoffee.com/AmruthPillai
|
||||
|
||||
|
||||

|
||||
<a href="https://pillai.xyz/digitalocean">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px" />
|
||||
</a>
|
||||
|
||||
### Appreciation
|
||||
## License
|
||||
|
||||
Thank you to everyone who made this project possible, including the many users who voiced their opinions, created issues and PRs to the original Reactive Resume project, and helped me along the way to make this a reality.
|
||||
Reactive Resume is packaged and distributed using the [MIT License](https://choosealicense.com/licenses/mit/) which allows for commercial use, distribution, modification and private use provided that all copies of the software contain the same license and copyright.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
###### Made with Love by [Amruth Pillai](https://amruthpillai.com/)
|
||||
_By the community, for the community._
|
||||
A passion project by [Amruth Pillai](https://amruthpillai.com/)
|
||||
|
||||
12
SECURITY.md
@ -1,12 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.0.0 | :white_check_mark: |
|
||||
| 1.0.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues) tab to report a vulnerability
|
||||
@ -1,17 +0,0 @@
|
||||
import { delay } from '../../src/utils/index';
|
||||
|
||||
const ReachRouter = jest.requireActual('@reach/router');
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const navigate = async (to, options) => {
|
||||
await delay(defaultDelayInMilliseconds);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...ReachRouter,
|
||||
navigate: jest.fn(navigate),
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('sets current user to anonymous user 1', async () => {
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeTruthy();
|
||||
expect(currentUser.uid).toEqual(AuthConstants.anonymousUser1.uid);
|
||||
});
|
||||
|
||||
test('calls onAuthStateChanged observer with anonymous user 1', async () => {
|
||||
let user = null;
|
||||
let error = null;
|
||||
FirebaseStub.auth().onAuthStateChanged(
|
||||
(_user) => {
|
||||
user = _user;
|
||||
},
|
||||
(_error) => {
|
||||
error = _error;
|
||||
},
|
||||
);
|
||||
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test('returns anonymous user 1', async () => {
|
||||
const user = await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid);
|
||||
});
|
||||
@ -1,54 +0,0 @@
|
||||
import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
describe('with Google auth provider', () => {
|
||||
test('sets current user to Google user 3', async () => {
|
||||
await FirebaseStub.auth().signInWithPopup(
|
||||
new FirebaseStub.auth.GoogleAuthProvider(),
|
||||
);
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeTruthy();
|
||||
expect(currentUser.uid).toEqual(AuthConstants.googleUser3.uid);
|
||||
});
|
||||
|
||||
test('sets current user provider data', async () => {
|
||||
const provider = new FirebaseStub.auth.GoogleAuthProvider();
|
||||
await FirebaseStub.auth().signInWithPopup(provider);
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeTruthy();
|
||||
expect(currentUser.providerData).toBeTruthy();
|
||||
expect(currentUser.providerData).toHaveLength(1);
|
||||
expect(currentUser.providerData[0].providerId).toEqual(provider.providerId);
|
||||
});
|
||||
|
||||
test('calls onAuthStateChanged observer with Google user 3', async () => {
|
||||
let user = null;
|
||||
let error = null;
|
||||
FirebaseStub.auth().onAuthStateChanged(
|
||||
(_user) => {
|
||||
user = _user;
|
||||
},
|
||||
(_error) => {
|
||||
error = _error;
|
||||
},
|
||||
);
|
||||
|
||||
await FirebaseStub.auth().signInWithPopup(
|
||||
new FirebaseStub.auth.GoogleAuthProvider(),
|
||||
);
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.googleUser3.uid);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test('returns Google user 3', async () => {
|
||||
const user = await FirebaseStub.auth().signInWithPopup(
|
||||
new FirebaseStub.auth.GoogleAuthProvider(),
|
||||
);
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.googleUser3.uid);
|
||||
});
|
||||
});
|
||||
@ -1,29 +0,0 @@
|
||||
import FirebaseStub from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('sets current user to null', async () => {
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
await FirebaseStub.auth().signOut();
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeNull();
|
||||
});
|
||||
|
||||
test('calls onAuthStateChanged observer with null', async () => {
|
||||
let user = null;
|
||||
let error = null;
|
||||
FirebaseStub.auth().onAuthStateChanged(
|
||||
(_user) => {
|
||||
user = _user;
|
||||
},
|
||||
(_error) => {
|
||||
error = _error;
|
||||
},
|
||||
);
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
await FirebaseStub.auth().signOut();
|
||||
|
||||
expect(user).toBeNull();
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
@ -1,35 +0,0 @@
|
||||
import FirebaseStub from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Auth instance', () => {
|
||||
const auth1 = FirebaseStub.auth();
|
||||
const auth2 = FirebaseStub.auth();
|
||||
|
||||
expect(auth1.uuid).toBeTruthy();
|
||||
expect(auth2.uuid).toBeTruthy();
|
||||
expect(auth1.uuid).toEqual(auth2.uuid);
|
||||
});
|
||||
|
||||
test('onAuthStateChanged unsubscribe removes observer', () => {
|
||||
const observer = () => {};
|
||||
const unsubscribe = FirebaseStub.auth().onAuthStateChanged(observer);
|
||||
expect(unsubscribe).toBeTruthy();
|
||||
expect(
|
||||
FirebaseStub.auth().onAuthStateChangedObservers.indexOf(observer),
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
|
||||
unsubscribe();
|
||||
|
||||
expect(
|
||||
FirebaseStub.auth().onAuthStateChangedObservers.indexOf(observer),
|
||||
).not.toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('current user delete calls signOut', async () => {
|
||||
const mockSignOut = jest.spyOn(FirebaseStub.auth(), 'signOut');
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
|
||||
await currentUser.delete();
|
||||
|
||||
expect(mockSignOut).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -1,53 +0,0 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('can filter resumes by user', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(DatabaseConstants.user1.uid)
|
||||
.on('value', (snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
snapshotValue ? Promise.resolve(true) : Promise.reject(),
|
||||
);
|
||||
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(Object.keys(snapshotValue)).toHaveLength(2);
|
||||
Object.values(snapshotValue).forEach((resume) =>
|
||||
expect(resume.user).toEqual(DatabaseConstants.user1.uid),
|
||||
);
|
||||
});
|
||||
|
||||
test('previously set query parameters are not kept when retrieving reference again', () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let reference = null;
|
||||
|
||||
reference = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
|
||||
expect(reference).toBeTruthy();
|
||||
const { uuid } = reference;
|
||||
expect(reference.orderByChildPath).toHaveLength(0);
|
||||
expect(reference.equalToValue).toHaveLength(0);
|
||||
|
||||
reference = FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo('testuser1');
|
||||
expect(reference).toBeTruthy();
|
||||
expect(reference.uuid).toBe(uuid);
|
||||
expect(reference.orderByChildPath).toBe('user');
|
||||
expect(reference.equalToValue).toBe('testuser1');
|
||||
|
||||
reference = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
|
||||
expect(reference).toBeTruthy();
|
||||
expect(reference.uuid).toBe(uuid);
|
||||
expect(reference.orderByChildPath).toHaveLength(0);
|
||||
expect(reference.equalToValue).toHaveLength(0);
|
||||
});
|
||||
@ -1,51 +0,0 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('removes event callbacks', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const userUid = DatabaseConstants.user1.uid;
|
||||
|
||||
let valueCallbackSnapshotValue = null;
|
||||
const valueCallback = jest.fn((snapshot) => {
|
||||
valueCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('value', valueCallback);
|
||||
await waitFor(() => valueCallback.mock.calls[0][0]);
|
||||
valueCallback.mockClear();
|
||||
valueCallbackSnapshotValue = null;
|
||||
|
||||
let childRemovedCallbackSnapshotValue = null;
|
||||
const childRemovedCallback = jest.fn((snapshot) => {
|
||||
childRemovedCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('child_removed', childRemovedCallback);
|
||||
|
||||
const removedResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(removedResume).toBeTruthy();
|
||||
|
||||
FirebaseStub.database().ref(DatabaseConstants.resumesPath).off();
|
||||
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${removedResume.id}`)
|
||||
.remove();
|
||||
|
||||
expect(childRemovedCallback.mock.calls).toHaveLength(0);
|
||||
expect(childRemovedCallbackSnapshotValue).toBeNull();
|
||||
expect(valueCallback.mock.calls).toHaveLength(0);
|
||||
expect(valueCallbackSnapshotValue).toBeNull();
|
||||
});
|
||||
@ -1,45 +0,0 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('triggers event with true if on the connected reference path', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.connectedPath)
|
||||
.on('value', (snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
snapshotValue ? Promise.resolve(true) : Promise.reject(),
|
||||
);
|
||||
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(snapshotValue).toBe(true);
|
||||
});
|
||||
|
||||
test('triggers event with resumes if on the resumes reference path', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumesDataSnapshot = await FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.once('value');
|
||||
const resumes = resumesDataSnapshot.val();
|
||||
|
||||
let snapshotValue = null;
|
||||
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.on('value', (snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
snapshotValue ? Promise.resolve(true) : Promise.reject(),
|
||||
);
|
||||
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(snapshotValue).toEqual(resumes);
|
||||
});
|
||||
@ -1,59 +0,0 @@
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('retrieves resume if it exists', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = DatabaseConstants.demoStateResume1Id;
|
||||
|
||||
const resume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(resume).toBeTruthy();
|
||||
expect(resume.id).toEqual(resumeId);
|
||||
});
|
||||
|
||||
test('retrieves null if resume does not exist', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = 'invalidResumeId';
|
||||
|
||||
const resume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(resume).toBeNull();
|
||||
});
|
||||
|
||||
test('retrieves user if it exists', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const expectedUser = DatabaseConstants.user1;
|
||||
|
||||
const user = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.usersPath}/${expectedUser.uid}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user).toEqual(expectedUser);
|
||||
});
|
||||
|
||||
test('retrieves null if user does not exist', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const userId = 'invalidUserId';
|
||||
|
||||
const user = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.usersPath}/${userId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
@ -1,77 +0,0 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('deletes data', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = DatabaseConstants.demoStateResume1Id;
|
||||
const removedResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(removedResume).toBeTruthy();
|
||||
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.remove();
|
||||
|
||||
const actualResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(actualResume).toBeNull();
|
||||
});
|
||||
|
||||
test('triggers events', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const userUid = DatabaseConstants.user1.uid;
|
||||
|
||||
let valueCallbackSnapshotValue = null;
|
||||
const valueCallback = jest.fn((snapshot) => {
|
||||
valueCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('value', valueCallback);
|
||||
await waitFor(() => valueCallback.mock.calls[0][0]);
|
||||
valueCallback.mockClear();
|
||||
valueCallbackSnapshotValue = null;
|
||||
|
||||
let childRemovedCallbackSnapshotValue = null;
|
||||
const childRemovedCallback = jest.fn((snapshot) => {
|
||||
childRemovedCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('child_removed', childRemovedCallback);
|
||||
|
||||
const removedResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(removedResume).toBeTruthy();
|
||||
expect(removedResume.user).toEqual(userUid);
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${removedResume.id}`)
|
||||
.remove();
|
||||
|
||||
await waitFor(() => childRemovedCallback.mock.calls[0][0]);
|
||||
expect(childRemovedCallback.mock.calls).toHaveLength(1);
|
||||
expect(childRemovedCallbackSnapshotValue).toBeTruthy();
|
||||
expect(childRemovedCallbackSnapshotValue).toEqual(removedResume);
|
||||
|
||||
await waitFor(() => valueCallback.mock.calls[0][0]);
|
||||
expect(valueCallback.mock.calls).toHaveLength(1);
|
||||
expect(valueCallbackSnapshotValue).toBeTruthy();
|
||||
expect(removedResume.id in valueCallbackSnapshotValue).toBe(false);
|
||||
});
|
||||
@ -1,71 +0,0 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('inserts data', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
const newResume = JSON.parse(JSON.stringify(existingResume));
|
||||
newResume.id = 'newre1';
|
||||
newResume.name = `Test Resume ${newResume.id}`;
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
|
||||
.set(newResume);
|
||||
|
||||
const actualResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(actualResume).toBeTruthy();
|
||||
expect(actualResume).toEqual(newResume);
|
||||
});
|
||||
|
||||
test('triggers events', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
const callback = jest.fn((snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(DatabaseConstants.user1.uid)
|
||||
.on('value', callback);
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
callback.mockClear();
|
||||
snapshotValue = null;
|
||||
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
const newResume = JSON.parse(JSON.stringify(existingResume));
|
||||
newResume.id = 'newre1';
|
||||
newResume.name = `Test Resume ${newResume.id}`;
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
|
||||
.set(newResume);
|
||||
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
|
||||
expect(callback.mock.calls).toHaveLength(1);
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(Object.keys(snapshotValue)).toHaveLength(3);
|
||||
expect(snapshotValue[newResume.id]).toBeTruthy();
|
||||
expect(snapshotValue[newResume.id]).toEqual(newResume);
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Reference instance', () => {
|
||||
const ref1 = FirebaseStub.database().ref(
|
||||
`${DatabaseConstants.resumesPath}/123`,
|
||||
);
|
||||
const ref2 = FirebaseStub.database().ref(
|
||||
`${DatabaseConstants.resumesPath}/123`,
|
||||
);
|
||||
|
||||
expect(ref1).toBeTruthy();
|
||||
expect(ref2).toBeTruthy();
|
||||
expect(ref1).toEqual(ref2);
|
||||
});
|
||||
|
||||
test('leading slash in reference path is ignored', () => {
|
||||
const path = `${DatabaseConstants.resumesPath}/123`;
|
||||
|
||||
const ref1 = FirebaseStub.database().ref(path);
|
||||
expect(ref1).toBeTruthy();
|
||||
expect(ref1.path).toEqual(path);
|
||||
|
||||
const ref2 = FirebaseStub.database().ref(`/${path}`);
|
||||
expect(ref2).toBeTruthy();
|
||||
expect(ref2).toEqual(ref1);
|
||||
});
|
||||
@ -1,86 +0,0 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('can spy on it', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const referencePath = `${DatabaseConstants.resumesPath}/123456`;
|
||||
const functionSpy = jest.spyOn(
|
||||
FirebaseStub.database().ref(referencePath),
|
||||
'update',
|
||||
);
|
||||
const updateArgument = 'test value 123';
|
||||
|
||||
await FirebaseStub.database().ref(referencePath).update(updateArgument);
|
||||
|
||||
expect(functionSpy).toHaveBeenCalledTimes(1);
|
||||
const functionCallArgument = functionSpy.mock.calls[0][0];
|
||||
expect(functionCallArgument).toBeTruthy();
|
||||
expect(functionCallArgument).toEqual(updateArgument);
|
||||
});
|
||||
|
||||
test('updates data', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = DatabaseConstants.demoStateResume1Id;
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
const resumeName = 'Test Resume renamed';
|
||||
existingResume.name = resumeName;
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.update(existingResume);
|
||||
|
||||
const actualResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(actualResume).toBeTruthy();
|
||||
expect(existingResume).toEqual(actualResume);
|
||||
expect(actualResume.name).toEqual(resumeName);
|
||||
});
|
||||
|
||||
test('triggers events', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
const callback = jest.fn((snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(DatabaseConstants.user1.uid)
|
||||
.on('value', callback);
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
callback.mockClear();
|
||||
snapshotValue = null;
|
||||
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
existingResume.name = 'Test Resume renamed';
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${existingResume.id}`)
|
||||
.update(existingResume);
|
||||
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
|
||||
expect(callback.mock.calls).toHaveLength(1);
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(Object.keys(snapshotValue)).toHaveLength(2);
|
||||
expect(snapshotValue[existingResume.id]).toBeTruthy();
|
||||
expect(snapshotValue[existingResume.id]).toEqual(existingResume);
|
||||
});
|
||||
@ -1,67 +0,0 @@
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Database instance', () => {
|
||||
const database1 = FirebaseStub.database();
|
||||
const database2 = FirebaseStub.database();
|
||||
|
||||
expect(database1.uuid).toBeTruthy();
|
||||
expect(database2.uuid).toBeTruthy();
|
||||
expect(database1.uuid).toEqual(database2.uuid);
|
||||
});
|
||||
|
||||
test('ServerValue.TIMESTAMP returns current time in milliseconds', () => {
|
||||
const now = new Date().getTime();
|
||||
const timestamp = FirebaseStub.database.ServerValue.TIMESTAMP;
|
||||
|
||||
expect(timestamp).toBeTruthy();
|
||||
expect(timestamp).toBeGreaterThanOrEqual(now);
|
||||
});
|
||||
|
||||
test('initializing data sets up resumes and users', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumesRef = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
|
||||
const resumesDataSnapshot = await resumesRef.once('value');
|
||||
const resumes = resumesDataSnapshot.val();
|
||||
expect(resumes).toBeTruthy();
|
||||
expect(Object.keys(resumes)).toHaveLength(5);
|
||||
const demoStateResume1 = resumes[DatabaseConstants.demoStateResume1Id];
|
||||
expect(demoStateResume1).toBeTruthy();
|
||||
expect(demoStateResume1.id).toEqual(DatabaseConstants.demoStateResume1Id);
|
||||
expect(demoStateResume1.user).toEqual(DatabaseConstants.user1.uid);
|
||||
const demoStateResume2 = resumes[DatabaseConstants.demoStateResume2Id];
|
||||
expect(demoStateResume2).toBeTruthy();
|
||||
expect(demoStateResume2.id).toEqual(DatabaseConstants.demoStateResume2Id);
|
||||
expect(demoStateResume2.user).toEqual(DatabaseConstants.user2.uid);
|
||||
const initialStateResume1 = resumes[DatabaseConstants.initialStateResume1Id];
|
||||
expect(initialStateResume1).toBeTruthy();
|
||||
expect(initialStateResume1.id).toEqual(
|
||||
DatabaseConstants.initialStateResume1Id,
|
||||
);
|
||||
expect(initialStateResume1.user).toEqual(DatabaseConstants.user1.uid);
|
||||
const demoStateResume3 = resumes[DatabaseConstants.demoStateResume3Id];
|
||||
expect(demoStateResume3).toBeTruthy();
|
||||
expect(demoStateResume3.id).toEqual(DatabaseConstants.demoStateResume3Id);
|
||||
expect(demoStateResume3.user).toEqual(DatabaseConstants.user3.uid);
|
||||
const initialStateResume2 = resumes[DatabaseConstants.initialStateResume2Id];
|
||||
expect(initialStateResume2).toBeTruthy();
|
||||
expect(initialStateResume2.id).toEqual(
|
||||
DatabaseConstants.initialStateResume2Id,
|
||||
);
|
||||
expect(initialStateResume2.user).toEqual(DatabaseConstants.user3.uid);
|
||||
|
||||
const usersRef = FirebaseStub.database().ref(DatabaseConstants.usersPath);
|
||||
const usersDataSnapshot = await usersRef.once('value');
|
||||
const users = usersDataSnapshot.val();
|
||||
expect(users).toBeTruthy();
|
||||
expect(Object.keys(users)).toHaveLength(3);
|
||||
const anonymousUser1 = users[DatabaseConstants.user1.uid];
|
||||
expect(anonymousUser1).toBeTruthy();
|
||||
expect(anonymousUser1).toEqual(DatabaseConstants.user1);
|
||||
const anonymousUser2 = users[DatabaseConstants.user2.uid];
|
||||
expect(anonymousUser2).toBeTruthy();
|
||||
expect(anonymousUser2).toEqual(DatabaseConstants.user2);
|
||||
const googleUser3 = users[DatabaseConstants.user3.uid];
|
||||
expect(googleUser3).toBeTruthy();
|
||||
expect(googleUser3).toEqual(DatabaseConstants.user3);
|
||||
});
|
||||
@ -1,19 +0,0 @@
|
||||
import FirebaseStub from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Functions instance', () => {
|
||||
const functions1 = FirebaseStub.functions();
|
||||
const functions2 = FirebaseStub.functions();
|
||||
|
||||
expect(functions1.uuid).toBeTruthy();
|
||||
expect(functions2.uuid).toBeTruthy();
|
||||
expect(functions1.uuid).toEqual(functions2.uuid);
|
||||
});
|
||||
|
||||
test('deleteUser function returns true', async () => {
|
||||
const deleteUser = FirebaseStub.functions().httpsCallable('deleteUser');
|
||||
|
||||
const result = await deleteUser();
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.data).toEqual(true);
|
||||
});
|
||||
@ -1,2 +0,0 @@
|
||||
const mockFile = 'test-file-stub';
|
||||
export default mockFile;
|
||||
@ -1,33 +0,0 @@
|
||||
import Auth from './gatsby-plugin-firebase/auth/auth';
|
||||
import AuthConstants from './gatsby-plugin-firebase/constants/auth';
|
||||
import Database from './gatsby-plugin-firebase/database/database';
|
||||
import DatabaseConstants from './gatsby-plugin-firebase/constants/database';
|
||||
import Functions from './gatsby-plugin-firebase/functions/functions';
|
||||
import FunctionsConstants from './gatsby-plugin-firebase/constants/functions';
|
||||
import GoogleAuthProvider from './gatsby-plugin-firebase/auth/googleAuthProvider';
|
||||
|
||||
class FirebaseStub {
|
||||
static auth() {
|
||||
return Auth.instance;
|
||||
}
|
||||
|
||||
static database() {
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
static functions() {
|
||||
return Functions.instance;
|
||||
}
|
||||
}
|
||||
|
||||
FirebaseStub.auth.GoogleAuthProvider = GoogleAuthProvider;
|
||||
|
||||
FirebaseStub.database.ServerValue = {};
|
||||
Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', {
|
||||
get() {
|
||||
return new Date().getTime();
|
||||
},
|
||||
});
|
||||
|
||||
export default FirebaseStub;
|
||||
export { AuthConstants, DatabaseConstants, FunctionsConstants };
|
||||
@ -1,125 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { delay } from '../../../src/utils/index';
|
||||
import AuthProvider from './authProvider';
|
||||
import Constants from '../constants/auth';
|
||||
import GoogleAuthProvider from './googleAuthProvider';
|
||||
import User from './user';
|
||||
|
||||
const singleton = Symbol('');
|
||||
const singletonEnforcer = Symbol('');
|
||||
|
||||
class Auth {
|
||||
constructor(enforcer) {
|
||||
if (enforcer !== singletonEnforcer) {
|
||||
throw new Error('Cannot construct singleton');
|
||||
}
|
||||
|
||||
this._uuid = uuidv4();
|
||||
this._currentUser = null;
|
||||
this._onAuthStateChangedObservers = [];
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this[singleton]) {
|
||||
this[singleton] = new Auth(singletonEnforcer);
|
||||
}
|
||||
|
||||
return this[singleton];
|
||||
}
|
||||
|
||||
get currentUser() {
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
get onAuthStateChangedObservers() {
|
||||
return this._onAuthStateChangedObservers;
|
||||
}
|
||||
|
||||
onAuthStateChanged(observer) {
|
||||
this.onAuthStateChangedObservers.push(observer);
|
||||
|
||||
return () => {
|
||||
this._onAuthStateChangedObservers =
|
||||
this.onAuthStateChangedObservers.filter((obs) => obs !== observer);
|
||||
};
|
||||
}
|
||||
|
||||
async signInAnonymously() {
|
||||
const user = Constants.anonymousUser1;
|
||||
|
||||
this._currentUser = new User(
|
||||
user.displayName,
|
||||
user.email,
|
||||
user.providerId,
|
||||
user.uid,
|
||||
user.isAnonymous,
|
||||
this.signOut.bind(this),
|
||||
);
|
||||
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
this.onAuthStateChangedObservers.forEach((observer) =>
|
||||
observer(this._currentUser),
|
||||
);
|
||||
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with popup.
|
||||
*
|
||||
* @param {AuthProvider} provider The provider to authenticate.
|
||||
*/
|
||||
async signInWithPopup(provider) {
|
||||
if (!provider) {
|
||||
throw new Error('provider must be provided.');
|
||||
} else if (!(provider instanceof AuthProvider)) {
|
||||
throw new Error('provider should be an AuthProvider.');
|
||||
}
|
||||
|
||||
if (!(provider instanceof GoogleAuthProvider)) {
|
||||
throw new Error(
|
||||
`${provider.constructor.name} is currently not supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
const user = Constants.googleUser3;
|
||||
|
||||
this._currentUser = new User(
|
||||
user.displayName,
|
||||
user.email,
|
||||
user.providerId,
|
||||
user.uid,
|
||||
user.isAnonymous,
|
||||
this.signOut.bind(this),
|
||||
);
|
||||
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
this.onAuthStateChangedObservers.forEach((observer) =>
|
||||
observer(this._currentUser),
|
||||
);
|
||||
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
if (this._currentUser === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentUser = null;
|
||||
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
this.onAuthStateChangedObservers.forEach((observer) => observer(null));
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
@ -1,23 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class AuthProvider {
|
||||
/**
|
||||
* Creates a new auth provider.
|
||||
*
|
||||
* @param {string} providerId Provider ID.
|
||||
*/
|
||||
constructor(providerId) {
|
||||
if (!providerId) {
|
||||
throw new Error('providerId must be provided.');
|
||||
} else if (typeof providerId !== 'string') {
|
||||
throw new Error('providerId should be a string.');
|
||||
} else {
|
||||
this._providerId = providerId;
|
||||
}
|
||||
}
|
||||
|
||||
get providerId() {
|
||||
return this._providerId;
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthProvider;
|
||||
@ -1,10 +0,0 @@
|
||||
import AuthProvider from './authProvider';
|
||||
import Constants from '../constants/auth';
|
||||
|
||||
class GoogleAuthProvider extends AuthProvider {
|
||||
constructor() {
|
||||
super(Constants.googleAuthProviderId);
|
||||
}
|
||||
}
|
||||
|
||||
export default GoogleAuthProvider;
|
||||
@ -1,67 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import Constants from '../constants/auth';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import AuthProvider from './authProvider';
|
||||
import UserInfo from './userInfo';
|
||||
import { delay } from '../../../src/utils/index';
|
||||
|
||||
class User extends UserInfo {
|
||||
/**
|
||||
* Creates a new user.
|
||||
*
|
||||
* @param {string|null} displayName Display name.
|
||||
* @param {string|null} email Email.
|
||||
* @param {string} providerId Auth provider ID.
|
||||
* @param {string} uid The user's unique ID.
|
||||
* @param {boolean} isAnonymous Is anonymous.
|
||||
* @param {function():Promise<void>} deleteUser Delete user callback.
|
||||
*/
|
||||
constructor(displayName, email, providerId, uid, isAnonymous, deleteUser) {
|
||||
super(displayName, email, providerId, uid);
|
||||
|
||||
if (!deleteUser) {
|
||||
throw new Error('deleteUser must be provided.');
|
||||
} else if (typeof deleteUser !== 'function') {
|
||||
throw new Error('deleteUser should be a function.');
|
||||
} else {
|
||||
this._deleteUser = deleteUser;
|
||||
}
|
||||
|
||||
this._isAnonymous = isAnonymous;
|
||||
|
||||
this._providerData = [];
|
||||
if (!isAnonymous) {
|
||||
this._providerData.push(
|
||||
new UserInfo(displayName, email, providerId, uid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get isAnonymous() {
|
||||
return this._isAnonymous;
|
||||
}
|
||||
|
||||
get providerData() {
|
||||
return this._providerData;
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
await this._deleteUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthenticates the user with popup.
|
||||
*
|
||||
* @param {AuthProvider} provider The provider to authenticate.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async reauthenticateWithPopup(provider) {
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
@ -1,47 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class UserInfo {
|
||||
/**
|
||||
* Creates a new user profile information.
|
||||
*
|
||||
* @param {string|null} displayName Display name.
|
||||
* @param {string|null} email Email.
|
||||
* @param {string} providerId Auth provider ID.
|
||||
* @param {string} uid The user's unique ID.
|
||||
*/
|
||||
constructor(displayName, email, providerId, uid) {
|
||||
if (!uid) {
|
||||
throw new Error('uid must be provided.');
|
||||
} else if (typeof uid !== 'string') {
|
||||
throw new Error('uid should be a string.');
|
||||
} else {
|
||||
this._uid = uid;
|
||||
}
|
||||
|
||||
if (typeof providerId !== 'string') {
|
||||
throw new Error('providerId should be a string.');
|
||||
} else {
|
||||
this._providerId = providerId;
|
||||
}
|
||||
|
||||
this._displayName = displayName;
|
||||
this._email = email;
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
return this._displayName;
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this._email;
|
||||
}
|
||||
|
||||
get providerId() {
|
||||
return this._providerId;
|
||||
}
|
||||
|
||||
get uid() {
|
||||
return this._uid;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserInfo;
|
||||
@ -1,51 +0,0 @@
|
||||
const googleAuthProviderId = 'google.com';
|
||||
|
||||
const anonymousUser1 = {
|
||||
displayName: 'Anonymous User 1',
|
||||
email: 'anonymous1@noemail.com',
|
||||
isAnonymous: true,
|
||||
providerId: '',
|
||||
uid: 'anonym1',
|
||||
};
|
||||
|
||||
const anonymousUser2 = {
|
||||
displayName: 'Anonymous User 2',
|
||||
email: 'anonymous2@noemail.com',
|
||||
isAnonymous: true,
|
||||
providerId: '',
|
||||
uid: 'anonym2',
|
||||
};
|
||||
|
||||
const googleUser3 = {
|
||||
displayName: 'Google User 3',
|
||||
email: 'google3@noemail.com',
|
||||
isAnonymous: false,
|
||||
providerId: googleAuthProviderId,
|
||||
uid: 'google3',
|
||||
};
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
class Auth {
|
||||
static get googleAuthProviderId() {
|
||||
return googleAuthProviderId;
|
||||
}
|
||||
|
||||
static get anonymousUser1() {
|
||||
return anonymousUser1;
|
||||
}
|
||||
|
||||
static get anonymousUser2() {
|
||||
return anonymousUser2;
|
||||
}
|
||||
|
||||
static get googleUser3() {
|
||||
return googleUser3;
|
||||
}
|
||||
|
||||
static get defaultDelayInMilliseconds() {
|
||||
return defaultDelayInMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
@ -1,89 +0,0 @@
|
||||
import AuthConstants from './auth';
|
||||
|
||||
const valueEventType = 'value';
|
||||
const childRemovedEventType = 'child_removed';
|
||||
|
||||
const resumesPath = 'resumes';
|
||||
const usersPath = 'users';
|
||||
const connectedPath = '.info/connected';
|
||||
|
||||
const demoStateResume1Id = 'demo_1';
|
||||
const demoStateResume2Id = 'demo_2';
|
||||
const demoStateResume3Id = 'demo_3';
|
||||
const initialStateResume1Id = 'init_1';
|
||||
const initialStateResume2Id = 'init_2';
|
||||
|
||||
const user1 = {
|
||||
uid: AuthConstants.anonymousUser1.uid,
|
||||
isAnonymous: AuthConstants.anonymousUser1.isAnonymous,
|
||||
};
|
||||
const user2 = {
|
||||
uid: AuthConstants.anonymousUser2.uid,
|
||||
isAnonymous: AuthConstants.anonymousUser2.isAnonymous,
|
||||
};
|
||||
const user3 = {
|
||||
uid: AuthConstants.googleUser3.uid,
|
||||
isAnonymous: AuthConstants.googleUser3.isAnonymous,
|
||||
};
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
class Database {
|
||||
static get valueEventType() {
|
||||
return valueEventType;
|
||||
}
|
||||
|
||||
static get childRemovedEventType() {
|
||||
return childRemovedEventType;
|
||||
}
|
||||
|
||||
static get resumesPath() {
|
||||
return resumesPath;
|
||||
}
|
||||
|
||||
static get usersPath() {
|
||||
return usersPath;
|
||||
}
|
||||
|
||||
static get connectedPath() {
|
||||
return connectedPath;
|
||||
}
|
||||
|
||||
static get demoStateResume1Id() {
|
||||
return demoStateResume1Id;
|
||||
}
|
||||
|
||||
static get demoStateResume2Id() {
|
||||
return demoStateResume2Id;
|
||||
}
|
||||
|
||||
static get demoStateResume3Id() {
|
||||
return demoStateResume3Id;
|
||||
}
|
||||
|
||||
static get initialStateResume1Id() {
|
||||
return initialStateResume1Id;
|
||||
}
|
||||
|
||||
static get initialStateResume2Id() {
|
||||
return initialStateResume2Id;
|
||||
}
|
||||
|
||||
static get user1() {
|
||||
return user1;
|
||||
}
|
||||
|
||||
static get user2() {
|
||||
return user2;
|
||||
}
|
||||
|
||||
static get user3() {
|
||||
return user3;
|
||||
}
|
||||
|
||||
static get defaultDelayInMilliseconds() {
|
||||
return defaultDelayInMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
@ -1,15 +0,0 @@
|
||||
const deleteUserFunctionName = 'deleteUser';
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
class Functions {
|
||||
static get deleteUserFunctionName() {
|
||||
return deleteUserFunctionName;
|
||||
}
|
||||
|
||||
static get defaultDelayInMilliseconds() {
|
||||
return defaultDelayInMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default Functions;
|
||||
@ -1,24 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class DataSnapshot {
|
||||
constructor(getData, value = undefined) {
|
||||
if (!getData) {
|
||||
throw new Error('getData must be provided.');
|
||||
} else if (typeof getData !== 'function') {
|
||||
throw new Error('getData should be a function.');
|
||||
}
|
||||
|
||||
this._getData = getData;
|
||||
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
val() {
|
||||
return typeof this.value !== 'undefined' ? this.value : this._getData();
|
||||
}
|
||||
}
|
||||
|
||||
export default DataSnapshot;
|
||||
@ -1,170 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import DatabaseConstants from '../constants/database';
|
||||
import Reference from './reference';
|
||||
|
||||
const singleton = Symbol('');
|
||||
const singletonEnforcer = Symbol('');
|
||||
|
||||
const readFile = (fileRelativePath) => {
|
||||
const fileAbsolutePath = path.resolve(__dirname, fileRelativePath);
|
||||
const fileBuffer = fs.readFileSync(fileAbsolutePath);
|
||||
const fileData = JSON.parse(fileBuffer);
|
||||
return fileData;
|
||||
};
|
||||
|
||||
class Database {
|
||||
constructor(enforcer) {
|
||||
if (enforcer !== singletonEnforcer) {
|
||||
throw new Error('Cannot construct singleton');
|
||||
}
|
||||
|
||||
this._uuid = uuidv4();
|
||||
this._data = {};
|
||||
this._references = {};
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this[singleton]) {
|
||||
this[singleton] = new Database(singletonEnforcer);
|
||||
}
|
||||
|
||||
return this[singleton];
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
_getData(dataPath) {
|
||||
if (!dataPath) {
|
||||
throw new Error('dataPath must be provided.');
|
||||
}
|
||||
|
||||
const dataPathElements = dataPath.split('/');
|
||||
if (!(dataPathElements[0] in this._data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dataPathElements.length === 1) {
|
||||
return this._data[dataPathElements[0]];
|
||||
}
|
||||
|
||||
if (dataPathElements.length === 2) {
|
||||
if (!(dataPathElements[1] in this._data[dataPathElements[0]])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._data[dataPathElements[0]][dataPathElements[1]];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_getReference(referencePath) {
|
||||
return referencePath in this._references
|
||||
? this._references[referencePath]
|
||||
: null;
|
||||
}
|
||||
|
||||
_setData(dataPath, value) {
|
||||
if (!dataPath) {
|
||||
throw new Error('dataPath must be provided.');
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
throw new Error('value is undefined.');
|
||||
}
|
||||
|
||||
const dataPathElements = dataPath.split('/');
|
||||
if (dataPathElements.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(dataPathElements[0] in this._data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataPathElements[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
delete this._data[dataPathElements[0]][dataPathElements[1]];
|
||||
} else {
|
||||
this._data[dataPathElements[0]][dataPathElements[1]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
initializeData() {
|
||||
const resumes = {};
|
||||
const date = new Date('December 15, 2020 11:20:25');
|
||||
|
||||
const demoStateResume1 = readFile('../../../src/data/demoState.json');
|
||||
demoStateResume1.updatedAt = date.valueOf();
|
||||
date.setMonth(date.getMonth() - 2);
|
||||
demoStateResume1.createdAt = date.valueOf();
|
||||
demoStateResume1.user = DatabaseConstants.user1.uid;
|
||||
resumes[DatabaseConstants.demoStateResume1Id] = demoStateResume1;
|
||||
|
||||
const demoStateResume2 = JSON.parse(JSON.stringify(demoStateResume1));
|
||||
demoStateResume2.user = DatabaseConstants.user2.uid;
|
||||
resumes[DatabaseConstants.demoStateResume2Id] = demoStateResume2;
|
||||
|
||||
const initialStateResume1 = readFile('../../../src/data/initialState.json');
|
||||
initialStateResume1.updatedAt = date.valueOf();
|
||||
initialStateResume1.createdAt = date.valueOf();
|
||||
initialStateResume1.user = DatabaseConstants.user1.uid;
|
||||
resumes[DatabaseConstants.initialStateResume1Id] = initialStateResume1;
|
||||
|
||||
const demoStateResume3 = readFile('../../../src/data/demoState.json');
|
||||
demoStateResume3.updatedAt = date.valueOf();
|
||||
date.setMonth(date.getMonth() - 2);
|
||||
demoStateResume3.createdAt = date.valueOf();
|
||||
demoStateResume3.user = DatabaseConstants.user3.uid;
|
||||
resumes[DatabaseConstants.demoStateResume3Id] = demoStateResume3;
|
||||
|
||||
const initialStateResume2 = readFile('../../../src/data/initialState.json');
|
||||
initialStateResume2.updatedAt = date.valueOf();
|
||||
initialStateResume2.createdAt = date.valueOf();
|
||||
initialStateResume2.user = DatabaseConstants.user3.uid;
|
||||
resumes[DatabaseConstants.initialStateResume2Id] = initialStateResume2;
|
||||
|
||||
Object.keys(resumes).forEach((key) => {
|
||||
const resume = resumes[key];
|
||||
resume.id = key;
|
||||
resume.name = `Test Resume ${key}`;
|
||||
});
|
||||
|
||||
this._data[DatabaseConstants.resumesPath] = resumes;
|
||||
|
||||
const users = {};
|
||||
users[DatabaseConstants.user1.uid] = DatabaseConstants.user1;
|
||||
users[DatabaseConstants.user2.uid] = DatabaseConstants.user2;
|
||||
users[DatabaseConstants.user3.uid] = DatabaseConstants.user3;
|
||||
this._data[DatabaseConstants.usersPath] = users;
|
||||
}
|
||||
|
||||
ref(referencePath) {
|
||||
const newRef = new Reference(
|
||||
referencePath,
|
||||
(dataPath) => this._getData(dataPath),
|
||||
(dataPath, value) => this._setData(dataPath, value),
|
||||
(refPath) => this._getReference(refPath),
|
||||
);
|
||||
|
||||
const existingRef = this._getReference(newRef.path);
|
||||
if (existingRef) {
|
||||
existingRef.initializeQueryParameters();
|
||||
return existingRef;
|
||||
}
|
||||
|
||||
this._references[newRef.path] = newRef;
|
||||
return newRef;
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
@ -1,215 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { delay } from '../../../src/utils/index';
|
||||
import DataSnapshot from './dataSnapshot';
|
||||
import DatabaseConstants from '../constants/database';
|
||||
|
||||
const parsePath = (path) => {
|
||||
if (!path) {
|
||||
throw new Error('path must be provided.');
|
||||
} else if (typeof path !== 'string') {
|
||||
throw new Error('path should be a string.');
|
||||
} else {
|
||||
let parsedPath = path.trim();
|
||||
|
||||
if (parsedPath[0] === '/') {
|
||||
parsedPath = parsedPath.substring(1);
|
||||
}
|
||||
|
||||
return parsedPath;
|
||||
}
|
||||
};
|
||||
|
||||
class Reference {
|
||||
constructor(path, getDatabaseData, setDatabaseData, getReference) {
|
||||
this._path = parsePath(path);
|
||||
|
||||
this._uuid = uuidv4();
|
||||
|
||||
if (this.path === DatabaseConstants.connectedPath) {
|
||||
this._dataSnapshot = new DataSnapshot(() => {}, true);
|
||||
} else {
|
||||
this._dataSnapshot = new DataSnapshot(() => this._getData());
|
||||
}
|
||||
|
||||
if (!getDatabaseData) {
|
||||
throw new Error('getDatabaseData must be provided.');
|
||||
} else if (typeof getDatabaseData !== 'function') {
|
||||
throw new Error('getDatabaseData should be a function.');
|
||||
}
|
||||
|
||||
this._getDatabaseData = getDatabaseData;
|
||||
|
||||
if (!setDatabaseData) {
|
||||
throw new Error('setDatabaseData must be provided.');
|
||||
} else if (typeof getDatabaseData !== 'function') {
|
||||
throw new Error('setDatabaseData should be a function.');
|
||||
}
|
||||
|
||||
this._setDatabaseData = setDatabaseData;
|
||||
|
||||
if (!getReference) {
|
||||
throw new Error('getReference must be provided.');
|
||||
} else if (typeof getDatabaseData !== 'function') {
|
||||
throw new Error('getReference should be a function.');
|
||||
}
|
||||
|
||||
this._getReference = getReference;
|
||||
|
||||
this._eventCallbacks = {};
|
||||
|
||||
this.initializeQueryParameters();
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this._path;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
get eventCallbacks() {
|
||||
return this._eventCallbacks;
|
||||
}
|
||||
|
||||
get orderByChildPath() {
|
||||
return this._orderByChildPath;
|
||||
}
|
||||
|
||||
get equalToValue() {
|
||||
return this._equalToValue;
|
||||
}
|
||||
|
||||
_getData() {
|
||||
const databaseData = this._getDatabaseData(this.path);
|
||||
|
||||
if (!databaseData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.orderByChildPath && this.equalToValue) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(databaseData).filter(
|
||||
([, value]) => value[this.orderByChildPath] === this.equalToValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return databaseData;
|
||||
}
|
||||
|
||||
_getParent() {
|
||||
const pathElements = this.path.split('/');
|
||||
|
||||
let parent = null;
|
||||
if (pathElements.length === 2) {
|
||||
parent = this._getReference(pathElements[0]);
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
_handleDataUpdate(value) {
|
||||
if (typeof value === 'undefined') {
|
||||
throw new Error('value must be provided.');
|
||||
}
|
||||
|
||||
const currentData = this._getData();
|
||||
const parentReference = this._getParent();
|
||||
|
||||
this._setDatabaseData(this.path, value);
|
||||
|
||||
if (value === null) {
|
||||
if (parentReference) {
|
||||
parentReference.triggerEventCallback(
|
||||
DatabaseConstants.childRemovedEventType,
|
||||
currentData,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.triggerEventCallback(DatabaseConstants.valueEventType);
|
||||
}
|
||||
|
||||
if (parentReference) {
|
||||
parentReference.triggerEventCallback(DatabaseConstants.valueEventType);
|
||||
}
|
||||
}
|
||||
|
||||
triggerEventCallback(eventType, snapshotValue = undefined) {
|
||||
if (!(eventType in this.eventCallbacks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot =
|
||||
this.path === DatabaseConstants.connectedPath
|
||||
? this._dataSnapshot
|
||||
: new DataSnapshot(() => this._getData(), snapshotValue);
|
||||
|
||||
this.eventCallbacks[eventType](snapshot);
|
||||
}
|
||||
|
||||
equalTo(value) {
|
||||
this._equalToValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
initializeQueryParameters() {
|
||||
this._orderByChildPath = '';
|
||||
this._equalToValue = '';
|
||||
}
|
||||
|
||||
off() {
|
||||
this._eventCallbacks = {};
|
||||
}
|
||||
|
||||
on(eventType, callback) {
|
||||
this.eventCallbacks[eventType] = callback;
|
||||
|
||||
if (eventType === DatabaseConstants.valueEventType) {
|
||||
setTimeout(() => {
|
||||
this.triggerEventCallback(eventType);
|
||||
}, DatabaseConstants.defaultDelayInMilliseconds);
|
||||
}
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
async once(eventType) {
|
||||
if (!eventType) {
|
||||
throw new Error('eventType must be provided.');
|
||||
} else if (typeof eventType !== 'string') {
|
||||
throw new Error('eventType should be a string.');
|
||||
}
|
||||
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
return this._dataSnapshot;
|
||||
}
|
||||
|
||||
orderByChild(path) {
|
||||
this._orderByChildPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
async update(value) {
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
this._handleDataUpdate(value);
|
||||
}
|
||||
|
||||
async remove() {
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
this._handleDataUpdate(null);
|
||||
}
|
||||
|
||||
async set(value) {
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
this._handleDataUpdate(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default Reference;
|
||||
@ -1,53 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import FunctionsConstants from '../constants/functions';
|
||||
import HttpsCallableResult from './httpsCallableResult';
|
||||
import { delay } from '../../../src/utils/index';
|
||||
|
||||
const singleton = Symbol('');
|
||||
const singletonEnforcer = Symbol('');
|
||||
|
||||
const deleteUser = async () => {
|
||||
await delay(FunctionsConstants.defaultDelayInMilliseconds);
|
||||
|
||||
return new HttpsCallableResult(true);
|
||||
};
|
||||
|
||||
class Functions {
|
||||
constructor(enforcer) {
|
||||
if (enforcer !== singletonEnforcer) {
|
||||
throw new Error('Cannot construct singleton');
|
||||
}
|
||||
|
||||
this._uuid = uuidv4();
|
||||
|
||||
this._httpsCallables = {};
|
||||
this._httpsCallables[FunctionsConstants.deleteUserFunctionName] =
|
||||
deleteUser;
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this[singleton]) {
|
||||
this[singleton] = new Functions(singletonEnforcer);
|
||||
}
|
||||
|
||||
return this[singleton];
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
httpsCallable(name) {
|
||||
if (!name) {
|
||||
throw new Error('name must be provided.');
|
||||
} else if (typeof name !== 'string') {
|
||||
throw new Error('name should be a string.');
|
||||
}
|
||||
|
||||
return this._httpsCallables[name];
|
||||
}
|
||||
}
|
||||
|
||||
export default Functions;
|
||||
@ -1,19 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class HttpsCallableResult {
|
||||
constructor(data) {
|
||||
this._uuid = uuidv4();
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpsCallableResult;
|
||||
@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { delay } from '../src/utils/index';
|
||||
|
||||
const Gatsby = jest.requireActual('gatsby');
|
||||
|
||||
const imageData = {
|
||||
images: {
|
||||
fallback: {
|
||||
src: `image_src.jpg`,
|
||||
srcSet: `image_src_set.jpg 1x`,
|
||||
},
|
||||
},
|
||||
layout: `fixed`,
|
||||
width: 1,
|
||||
height: 2,
|
||||
};
|
||||
const childImageSharp = { gatsbyImageData: imageData };
|
||||
|
||||
const useStaticQuery = () => ({
|
||||
site: {
|
||||
siteMetadata: {
|
||||
title: 'Test title',
|
||||
description: 'Test description',
|
||||
author: 'Test author',
|
||||
siteUrl: 'https://testsiteurl/',
|
||||
},
|
||||
},
|
||||
file: {
|
||||
childImageSharp,
|
||||
},
|
||||
onyx: {
|
||||
childImageSharp,
|
||||
},
|
||||
pikachu: {
|
||||
childImageSharp,
|
||||
},
|
||||
gengar: {
|
||||
childImageSharp,
|
||||
},
|
||||
castform: {
|
||||
childImageSharp,
|
||||
},
|
||||
glalie: {
|
||||
childImageSharp,
|
||||
},
|
||||
celebi: {
|
||||
childImageSharp,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
const navigate = async () => {
|
||||
await delay(defaultDelayInMilliseconds);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...Gatsby,
|
||||
graphql: jest.fn(),
|
||||
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
|
||||
React.createElement('a', {
|
||||
...rest,
|
||||
href: to,
|
||||
}),
|
||||
),
|
||||
navigate: jest.fn(navigate),
|
||||
useStaticQuery,
|
||||
};
|
||||
33
app/.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.apk
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
8
app/app/.editorconfig
Normal file
@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
2
app/app/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/build
|
||||
/release
|
||||
46
app/app/build.gradle
Normal file
@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId "me.rxresu.app"
|
||||
minSdk 21
|
||||
targetSdk 32
|
||||
versionCode 2
|
||||
versionName "1.0"
|
||||
|
||||
resConfigs "en"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
zipAlignEnabled true
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
}
|
||||
21
app/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
26
app/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="me.rxresu.app">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ReactiveResume.NoActionBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.ReactiveResume.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
76
app/app/src/main/java/me/rxresu/app/MainActivity.kt
Normal file
@ -0,0 +1,76 @@
|
||||
package me.rxresu.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var webView: WebView
|
||||
|
||||
private var isLoaded: Boolean = false
|
||||
private var webURL = "https://rxresu.me"
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
webView = findViewById(R.id.webview)
|
||||
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.settings.userAgentString = "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Mobile Safari/537.36"
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (!isLoaded) loadWebView()
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
private fun loadWebView() {
|
||||
webView.loadUrl(webURL)
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
val url = request?.url.toString()
|
||||
view?.loadUrl(url)
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
isLoaded = true
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
||||
isLoaded = false
|
||||
super.onReceivedError(view, request, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
15
app/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<include
|
||||
layout="@layout/content_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
18
app/app/src/main/res/layout/content_main.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
5
app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
app/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
app/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
4
app/app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
3
app/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Reactive Resume</string>
|
||||
</resources>
|
||||
12
app/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<resources>
|
||||
<style name="Theme.ReactiveResume" parent="Theme.MaterialComponents.DayNight.DarkActionBar" />
|
||||
|
||||
<style name="Theme.ReactiveResume.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.ReactiveResume.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
|
||||
<style name="Theme.ReactiveResume.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||
</resources>
|
||||
9
app/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '7.1.2' apply false
|
||||
id 'com.android.library' version '7.1.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
23
app/gradle.properties
Normal file
@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||