mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7902f67f4f | |||
| 57dd110187 | |||
| 829375e87a | |||
| 0a15b4ebc9 | |||
| 2bff3fc20b | |||
| 1e997fe67c | |||
| dbf06455e4 | |||
| 42c7c9ade1 | |||
| 36c19bac3f | |||
| 44a9300aff | |||
| 610b5ba9d4 | |||
| 769e8811cd | |||
| 676fbcafe7 | |||
| 3935ae1e04 | |||
| ef6b765266 | |||
| 647dd6e682 | |||
| 43841e9962 | |||
| e2236c3207 | |||
| 7389d33ee5 | |||
| 4b21eabec9 | |||
| 1815b0fa21 | |||
| 6c4d3cbd56 | |||
| 8c2f3c8504 | |||
| 3aa7a98d9d | |||
| 5519ec898d | |||
| 1cd4c5d733 | |||
| 73d11c323f | |||
| 38812fcf25 | |||
| c22de12f12 | |||
| c94c971599 | |||
| c9a71a5917 | |||
| 8a29387470 | |||
| 592511b090 | |||
| af63fd38d4 | |||
| bf38b1b254 | |||
| 4a1c0079db | |||
| 5b6f6b7621 | |||
| 02587255fe | |||
| 9ef2a84ac2 | |||
| 77b1c5b536 | |||
| bf956fe18c | |||
| 4114f1e1dd | |||
| 668d39fa87 | |||
| 0d88a18757 | |||
| 0630369087 | |||
| 73af4a6859 | |||
| 99ddeb25a9 | |||
| 685aa06778 | |||
| 460abc6f1d | |||
| 04f02157ac | |||
| 828a4a8715 | |||
| 5b3141cd49 |
@ -1,13 +1,19 @@
|
||||
# Build Artifacts
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
|
||||
# Project Metadata
|
||||
.crowdin.yml
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
SECURITY.md
|
||||
CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
|
||||
18
.env.example
18
.env.example
@ -1,7 +1,11 @@
|
||||
# Turbo Cache (Optional)
|
||||
TURBO_TEAM=
|
||||
TURBO_TOKEN=
|
||||
|
||||
# Server + Client
|
||||
TZ=UTC
|
||||
PUBLIC_URL=http://localhost:3000
|
||||
PUBLIC_SERVER_URL=http://localhost:3000/api
|
||||
PUBLIC_URL=http://client:3000
|
||||
PUBLIC_SERVER_URL=http://server:3100
|
||||
PUBLIC_GOOGLE_CLIENT_ID=
|
||||
|
||||
# Server + Database
|
||||
@ -18,10 +22,12 @@ JWT_SECRET=
|
||||
JWT_EXPIRY_TIME=604800
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_API_KEY=
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
||||
SENDGRID_FROM_NAME=
|
||||
SENDGRID_FROM_EMAIL=
|
||||
MAIL_FROM_NAME=
|
||||
MAIL_FROM_EMAIL=
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
STORAGE_BUCKET=
|
||||
STORAGE_REGION=
|
||||
STORAGE_ENDPOINT=
|
||||
|
||||
@ -1,31 +1,34 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": ["/app"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"extends": ["plugin:@typescript-eslint/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
|
||||
// ESLint
|
||||
"no-unused-vars": "off",
|
||||
|
||||
// Unused Imports
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// Simple Import Sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
|
||||
// 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"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,36 +1,43 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help improve
|
||||
title: "[BUG] "
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Product Flavor**
|
||||
|
||||
- [ ] Managed (https://rxresu.me)
|
||||
- [ ] Self Hosted
|
||||
|
||||
**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]-->
|
||||
|
||||
- OS: <!--[e.g. iOS]-->
|
||||
- Browser <!--[e.g. chrome, safari]-->
|
||||
- Version <!--[e.g. 22]-->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,20 +1,23 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
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. -->
|
||||
|
||||
12
.github/workflows/docker-build-push.yml
vendored
12
.github/workflows/docker-build-push.yml
vendored
@ -9,9 +9,15 @@ jobs:
|
||||
name: Client
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
@ -46,9 +52,15 @@ jobs:
|
||||
name: Server
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
|
||||
88
.github/workflows/docker-build.yml
vendored
Normal file
88
.github/workflows/docker-build.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: Client
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: vars
|
||||
name: Get Short SHA
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Build Client Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ steps.vars.outputs.sha_short }}
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
server:
|
||||
name: Server
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Build Server Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ steps.vars.outputs.sha_short }}
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.vars.outputs.sha_short }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@ node_modules
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm install
|
||||
pnpm run lint
|
||||
pnpm run format
|
||||
@ -18,6 +18,7 @@ CHANGELOG.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
|
||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@ -1,3 +1,7 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@ -3,12 +3,12 @@
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Debug: Server",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"stopOnEntry": false,
|
||||
"protocol": "inspector"
|
||||
"protocol": "inspector",
|
||||
"stopOnEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug: Client",
|
||||
@ -17,9 +17,9 @@
|
||||
"command": "pnpm run dev:client",
|
||||
"console": "integratedTerminal",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
"uriFormat": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
@ -1,27 +1,18 @@
|
||||
{
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"editor.wordWrap": "on",
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.wordWrap": "on",
|
||||
"eslint.workingDirectories": [
|
||||
"schema",
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"react"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"client/public/locales"
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"scss.validate": false,
|
||||
"conventionalCommits.scopes": [
|
||||
"client",
|
||||
"server",
|
||||
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
@ -2,6 +2,17 @@
|
||||
|
||||
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.6.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.3...v3.6.2) (2022-08-25)
|
||||
|
||||
### Features
|
||||
|
||||
- Implement Undo/Redo functionality across the resume builder section
|
||||
|
||||
### Improvements
|
||||
|
||||
- Update dependencies to the latest version
|
||||
- Cleanup ESLint configuration, add tailwindCSS formatting
|
||||
|
||||
### [3.5.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.2...v3.5.3) (2022-08-11)
|
||||
|
||||
### [3.5.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.1...v3.5.2) (2022-08-04)
|
||||
|
||||
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
{
|
||||
"extends": ["../.eslintrc.json", "next/core-web-vitals"],
|
||||
"extends": ["../.eslintrc.json", "next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||
"ignorePatterns": [".next", "__ENV.js"],
|
||||
"settings": {
|
||||
"next": {
|
||||
"rootDir": "client"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
// Next.js
|
||||
"@next/next/no-img-element": "off",
|
||||
"@next/next/no-sync-scripts": "off",
|
||||
"@next/next/no-html-link-for-pages": ["error", "pages"]
|
||||
|
||||
// React Hooks
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
// Tailwind CSS
|
||||
"tailwindcss/no-custom-classname": ["warn", { "whitelist": ["preview-mode", "printer-mode", "markdown"] }]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
FROM node:lts-alpine as dependencies
|
||||
|
||||
RUN apk add --no-cache curl g++ make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
FROM node:lts-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-*.yaml ./
|
||||
RUN apk add --no-cache g++ git curl make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
FROM base as dependencies
|
||||
|
||||
COPY package.json pnpm-*.yaml turbo.json ./
|
||||
COPY ./schema/package.json ./schema/package.json
|
||||
COPY ./client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM node:lts-alpine as builder
|
||||
|
||||
RUN apk add --no-cache curl g++ make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
WORKDIR /app
|
||||
FROM base as builder
|
||||
|
||||
COPY . .
|
||||
|
||||
@ -24,21 +21,20 @@ COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
|
||||
COPY --from=dependencies /app/client/node_modules ./client/node_modules
|
||||
|
||||
RUN pnpm run build:schema
|
||||
RUN pnpm run build:client
|
||||
ARG TURBO_TEAM
|
||||
ARG TURBO_TOKEN
|
||||
|
||||
FROM node:lts-alpine as production
|
||||
ENV TURBO_TEAM $TURBO_TEAM
|
||||
ENV TURBO_TOKEN $TURBO_TOKEN
|
||||
|
||||
WORKDIR /app
|
||||
RUN pnpm run build --filter client
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
FROM base as production
|
||||
|
||||
COPY --from=builder /app/pnpm-*.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/package.json /app/pnpm-*.yaml /app/turbo.json ./
|
||||
COPY --from=builder /app/client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install -F client --frozen-lockfile --prod
|
||||
RUN pnpm install --filter client --prod --frozen-lockfile --workspace-root
|
||||
|
||||
COPY --from=builder /app/client/.next ./client/.next
|
||||
COPY --from=builder /app/client/public ./client/public
|
||||
@ -52,4 +48,4 @@ ENV PORT 3000
|
||||
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||
CMD curl -fSs 127.0.0.1:3000 || exit 1
|
||||
|
||||
CMD [ "pnpm", "run", "start:client" ]
|
||||
CMD [ "pnpm", "run", "start", "--filter", "client" ]
|
||||
@ -5,6 +5,8 @@ import {
|
||||
FilterCenterFocus,
|
||||
InsertPageBreak,
|
||||
Link,
|
||||
RedoOutlined,
|
||||
UndoOutlined,
|
||||
ViewSidebar,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
@ -16,6 +18,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||
import { ActionCreators } from 'redux-undo';
|
||||
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
@ -31,14 +34,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const pages = useAppSelector((state) => state.resume.metadata.layout);
|
||||
|
||||
const { past, present: resume, future } = useAppSelector((state) => state.resume);
|
||||
const pages = get(resume, 'metadata.layout');
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
const handleUndo = () => dispatch(ActionCreators.undo());
|
||||
const handleRedo = () => dispatch(ActionCreators.redo());
|
||||
|
||||
const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine());
|
||||
|
||||
const handleTogglePageOrientation = () => dispatch(togglePageOrientation());
|
||||
@ -75,6 +82,20 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
})}
|
||||
>
|
||||
<div className={styles.controller}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.undo')}>
|
||||
<ButtonBase onClick={handleUndo} className={clsx({ 'pointer-events-none opacity-50': past.length < 2 })}>
|
||||
<UndoOutlined fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.redo')}>
|
||||
<ButtonBase onClick={handleRedo} className={clsx({ 'pointer-events-none opacity-50': future.length === 0 })}>
|
||||
<RedoOutlined fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}>
|
||||
<ButtonBase onClick={() => zoomIn(0.25)}>
|
||||
<ZoomIn fontSize="medium" />
|
||||
@ -97,17 +118,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
|
||||
{isDesktop && (
|
||||
<>
|
||||
{pages.length > 1 && (
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
||||
{orientation === 'vertical' ? (
|
||||
<AlignHorizontalCenter fontSize="medium" />
|
||||
) : (
|
||||
<AlignVerticalCenter fontSize="medium" />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||
<ButtonBase
|
||||
onClick={handleTogglePageOrientation}
|
||||
className={clsx({ 'pointer-events-none opacity-50': pages.length === 1 })}
|
||||
>
|
||||
{orientation === 'vertical' ? (
|
||||
<AlignHorizontalCenter fontSize="medium" />
|
||||
) : (
|
||||
<AlignVerticalCenter fontSize="medium" />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}>
|
||||
<ButtonBase onClick={handleTogglePageBreakLine}>
|
||||
|
||||
@ -13,7 +13,7 @@ import Page from './Page';
|
||||
const Center = () => {
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const layout: string[][][] = get(resume, 'metadata.layout');
|
||||
|
||||
if (isEmpty(resume)) return null;
|
||||
|
||||
@ -57,7 +57,7 @@ const Header = () => {
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
|
||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||
|
||||
@ -20,7 +20,7 @@ type Props = PageProps & {
|
||||
const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const breakLine: boolean = useAppSelector((state) => state.build.page.breakLine);
|
||||
|
||||
const theme: Theme = get(resume, 'metadata.theme');
|
||||
|
||||
@ -25,7 +25,7 @@ const LeftSidebar = () => {
|
||||
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
|
||||
|
||||
const sections = useAppSelector((state) => state.resume.sections);
|
||||
const sections = useAppSelector((state) => state.resume.present.sections);
|
||||
const { open } = useAppSelector((state) => state.build.sidebar.left);
|
||||
|
||||
const customSections = useMemo(() => getCustomSections(sections), [sections]);
|
||||
|
||||
@ -32,7 +32,7 @@ const Basics = () => {
|
||||
<PhotoUpload />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 w-full sm:col-span-2">
|
||||
<div className="grid w-full gap-2 sm:col-span-2">
|
||||
<ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
||||
|
||||
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
|
||||
|
||||
@ -12,7 +12,7 @@ const PhotoFilters = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||
const size: number = get(photo, 'filters.size', 128);
|
||||
const shape: PhotoShape = get(photo, 'filters.shape', 'square');
|
||||
const grayscale: boolean = get(photo, 'filters.grayscale', false);
|
||||
|
||||
@ -21,8 +21,8 @@ const PhotoUpload: React.FC = () => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const id: number = useAppSelector((state) => get(state.resume, 'id'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
||||
const id: number = useAppSelector((state) => get(state.resume.present, 'id'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||
|
||||
const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto);
|
||||
|
||||
|
||||
@ -37,8 +37,8 @@ const Section: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector<string>((state) => get(state.resume, `${path}.name`, name));
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume, `${path}.visible`, true));
|
||||
const heading = useAppSelector<string>((state) => get(state.resume.present, `${path}.name`, name));
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const handleAdd = () => {
|
||||
const id = path.split('.')[1];
|
||||
|
||||
@ -18,7 +18,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const columns = useAppSelector<number>((state) => get(state.resume, `${path}.columns`, 2));
|
||||
const columns = useAppSelector<number>((state) => get(state.resume.present, `${path}.columns`, 2));
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
|
||||
@ -17,7 +17,7 @@ const CustomCSS = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume, 'metadata.css', {}));
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {}));
|
||||
|
||||
const handleChange = (value: string | undefined) => {
|
||||
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
||||
|
||||
@ -13,7 +13,7 @@ import { useAppSelector } from '@/store/hooks';
|
||||
const Export = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { Add, Close, Restore } from '@mui/icons-material';
|
||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
@ -23,8 +23,8 @@ const Layout = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const layout = useAppSelector((state) => state.resume.metadata.layout);
|
||||
const resumeSections = useAppSelector((state) => state.resume.sections);
|
||||
const layout = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||
const resumeSections = useAppSelector((state) => state.resume.present.sections);
|
||||
|
||||
const onDragEnd = (dropResult: DropResult) => {
|
||||
const { source: srcLoc, destination: destLoc } = dropResult;
|
||||
|
||||
@ -38,9 +38,9 @@ const Settings = () => {
|
||||
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const theme = useAppSelector((state) => state.build.theme);
|
||||
const pages = useAppSelector((state) => state.resume.metadata.layout);
|
||||
const pages = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||
const breakLine = useAppSelector((state) => state.build.page.breakLine);
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
@ -97,7 +97,7 @@ const Settings = () => {
|
||||
<List sx={{ padding: 0 }}>
|
||||
{/* Global Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.global.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
@ -155,7 +155,7 @@ const Settings = () => {
|
||||
|
||||
{/* Page Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.page.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
@ -187,7 +187,7 @@ const Settings = () => {
|
||||
|
||||
{/* Resume Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ const Sharing = () => {
|
||||
|
||||
const [showShortUrl, setShowShortUrl] = useState(false);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const isPublic = useMemo(() => get(resume, 'public'), [resume]);
|
||||
const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]);
|
||||
const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]);
|
||||
|
||||
@ -16,7 +16,7 @@ const Templates = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const currentTemplate: string = useAppSelector((state) => get(state.resume, 'metadata.template'));
|
||||
const currentTemplate: string = useAppSelector((state) => get(state.resume.present, 'metadata.template'));
|
||||
|
||||
const handleChange = (template: TemplateMeta) => {
|
||||
dispatch(setResumeState({ path: 'metadata.template', value: template.id }));
|
||||
@ -31,7 +31,7 @@ const Templates = () => {
|
||||
<div key={template.id} className={styles.template}>
|
||||
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
|
||||
<ButtonBase onClick={() => handleChange(template)}>
|
||||
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" />
|
||||
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.container {
|
||||
@apply grid sm:grid-cols-2 gap-4;
|
||||
@apply grid gap-4 sm:grid-cols-2;
|
||||
}
|
||||
|
||||
.colorOptions {
|
||||
@apply col-span-2 mb-4;
|
||||
@apply grid grid-cols-8 gap-y-2 justify-items-center;
|
||||
@apply grid grid-cols-8 justify-items-center gap-y-2;
|
||||
}
|
||||
|
||||
@ -16,7 +16,9 @@ const Theme = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { background, text, primary } = useAppSelector<ThemeType>((state) => get(state.resume, 'metadata.theme'));
|
||||
const { background, text, primary } = useAppSelector<ThemeType>((state) =>
|
||||
get(state.resume.present, 'metadata.theme')
|
||||
);
|
||||
|
||||
const handleChange = (property: string, color: string) => {
|
||||
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color }));
|
||||
|
||||
@ -33,7 +33,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume, 'metadata.typography'));
|
||||
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume.present, 'metadata.typography'));
|
||||
|
||||
const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, {
|
||||
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
.testimony {
|
||||
@apply grid gap-2;
|
||||
@apply border-2 rounded p-4 dark:border-neutral-800;
|
||||
@apply rounded border-2 p-4 dark:border-neutral-800;
|
||||
|
||||
blockquote {
|
||||
@apply text-xs leading-normal text-justify opacity-90;
|
||||
@apply text-justify text-xs leading-normal opacity-90;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 left-0 right-0 z-50 pt-6 bg-neutral-50 dark:bg-neutral-900;
|
||||
@apply sticky top-0 left-0 right-0 z-50 bg-neutral-50 pt-6 dark:bg-neutral-900;
|
||||
@apply flex items-center justify-between;
|
||||
@apply w-full border-b pb-5 dark:border-white/10;
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
@apply sticky bottom-0 left-0 right-0 z-50 pb-6 bg-neutral-50 dark:bg-neutral-900;
|
||||
@apply sticky bottom-0 left-0 right-0 z-50 bg-neutral-50 pb-6 dark:bg-neutral-900;
|
||||
@apply flex items-center justify-end gap-x-4;
|
||||
@apply w-full border-t pt-5 dark:border-white/10;
|
||||
}
|
||||
|
||||
@ -32,8 +32,8 @@ const Heading: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`, name));
|
||||
const visibility = useAppSelector((state) => get(state.resume, `${path}.visible`, true));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`, name));
|
||||
const visibility = useAppSelector((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
|
||||
@ -9,5 +9,5 @@
|
||||
}
|
||||
|
||||
.language {
|
||||
@apply py-2 px-4 cursor-pointer text-center hover:underline;
|
||||
@apply cursor-pointer py-2 px-4 text-center hover:underline;
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ const List: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume, path, []));
|
||||
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume.present, path, []));
|
||||
|
||||
const handleEdit = (item: ListItemType) => {
|
||||
isFunction(onEdit) && onEdit(item);
|
||||
|
||||
@ -5,6 +5,7 @@ import styles from './Loading.module.scss';
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
const { isReady } = useRouter();
|
||||
|
||||
const isFetching = useIsFetching();
|
||||
const isMutating = useIsMutating();
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ interface Props {
|
||||
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const stateValue = useAppSelector((state) => get(state.resume, path, ''));
|
||||
const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
|
||||
|
||||
useEffect(() => {
|
||||
setValue(stateValue);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import { createTheme, ThemeOptions } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
const theme: ThemeOptions = {
|
||||
typography: {
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
@ -49,7 +49,7 @@ const theme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
...theme,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Google, Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Button, IconButton, InputAdornment, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
@ -17,8 +18,6 @@ import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
declare const google: any;
|
||||
|
||||
type FormData = {
|
||||
identifier: string;
|
||||
password: string;
|
||||
@ -85,28 +84,16 @@ const LoginModal: React.FC = () => {
|
||||
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
||||
};
|
||||
|
||||
const handleLoginWithGoogle = async () => {
|
||||
console.log(process.env.PUBLIC_GOOGLE_CLIENT_ID, env('GOOGLE_CLIENT_ID'));
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
|
||||
google.accounts.id.initialize({
|
||||
auto_select: true,
|
||||
itp_support: true,
|
||||
client_id: env('GOOGLE_CLIENT_ID'),
|
||||
callback: async (response: any) => {
|
||||
await loginWithGoogleMutation({ credential: response.credential });
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
handleClose();
|
||||
},
|
||||
});
|
||||
|
||||
google.accounts.id.prompt((notification: any) => {
|
||||
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
||||
const reason = notification.getNotDisplayedReason() || notification.getSkippedReason();
|
||||
|
||||
toast.error(`Google returned an error while trying to sign in: ${reason}.`);
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
}
|
||||
});
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
};
|
||||
|
||||
const PasswordVisibility = (): React.ReactElement => {
|
||||
@ -130,15 +117,7 @@ const LoginModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
startIcon={<Google />}
|
||||
onClick={handleLoginWithGoogle}
|
||||
>
|
||||
{t<string>('modals.auth.login.actions.google')}
|
||||
</Button>
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Google, HowToReg } from '@mui/icons-material';
|
||||
import { HowToReg } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
@ -14,8 +16,6 @@ import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
declare const google: any;
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
username: string;
|
||||
@ -79,18 +79,16 @@ const RegisterModal: React.FC = () => {
|
||||
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||
};
|
||||
|
||||
const handleLoginWithGoogle = async () => {
|
||||
google.accounts.id.initialize({
|
||||
client_id: env('GOOGLE_CLIENT_ID'),
|
||||
callback: async (response: any) => {
|
||||
await loginWithGoogleMutation({ credential: response.credential });
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
|
||||
handleClose();
|
||||
},
|
||||
auto_select: false,
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
google.accounts.id.prompt();
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -102,15 +100,7 @@ const RegisterModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
startIcon={<Google />}
|
||||
onClick={handleLoginWithGoogle}
|
||||
>
|
||||
{t<string>('modals.auth.register.actions.google')}
|
||||
</Button>
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
@ -44,7 +44,7 @@ const AwardModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -44,7 +44,7 @@ const CertificateModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ const CustomModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
|
||||
|
||||
const path: string = get(payload, 'path', '');
|
||||
|
||||
@ -57,7 +57,7 @@ const EducationModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -35,7 +35,7 @@ const InterestModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -36,7 +36,7 @@ const LanguageModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -53,7 +53,7 @@ const ProjectModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -44,7 +44,7 @@ const PublicationModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -41,7 +41,7 @@ const ReferenceModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -39,7 +39,7 @@ const SkillModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -50,7 +50,7 @@ const VolunteerModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -50,7 +50,7 @@ const WorkModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -69,7 +69,8 @@ const CreateResumeModal: React.FC = () => {
|
||||
try {
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
"name": "@reactive-resume/client",
|
||||
"scripts": {
|
||||
"dev": "react-env --prefix PUBLIC -- next dev",
|
||||
"lint": "next lint --fix",
|
||||
"build": "next build && npm run sitemap",
|
||||
"start": "react-env --prefix PUBLIC -- next start",
|
||||
"lint": "next lint --fix",
|
||||
"sitemap": "next-sitemap --config next-sitemap.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -13,33 +13,34 @@
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@emotion/styled": "^11.10.0",
|
||||
"@hello-pangea/dnd": "^16.0.0",
|
||||
"@hookform/resolvers": "2.9.7",
|
||||
"@monaco-editor/react": "^4.4.5",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/lab": "^5.0.0-alpha.94",
|
||||
"@mui/material": "^5.10.0",
|
||||
"@mui/system": "^5.10.0",
|
||||
"@mui/x-date-pickers": "5.0.0-beta.5",
|
||||
"@next/env": "^12.2.4",
|
||||
"@reduxjs/toolkit": "^1.8.4",
|
||||
"@mui/icons-material": "^5.10.2",
|
||||
"@mui/lab": "^5.0.0-alpha.96",
|
||||
"@mui/material": "^5.10.2",
|
||||
"@mui/system": "^5.10.2",
|
||||
"@mui/x-date-pickers": "5.0.0-beta.7",
|
||||
"@next/env": "^12.2.5",
|
||||
"@react-oauth/google": "^0.2.6",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"axios": "^0.27.2",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.4",
|
||||
"dayjs": "^1.11.5",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "12.2.4",
|
||||
"next-i18next": "^11.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-colorful": "^5.6.0",
|
||||
"next": "12.2.5",
|
||||
"next-i18next": "^12.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.34.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.34.2",
|
||||
"react-hot-toast": "2.3.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-icons": "^4.4.0",
|
||||
@ -49,33 +50,33 @@
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-saga": "^1.2.1",
|
||||
"redux-undo": "^1.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.30.7",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/node": "18.7.1",
|
||||
"@types/react": "18.0.17",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/node": "^18.7.13",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"csstype": "^3.1.0",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-next": "12.2.4",
|
||||
"next-sitemap": "^3.1.17",
|
||||
"eslint-config-next": "^12.2.5",
|
||||
"eslint-plugin-tailwindcss": "^3.6.0",
|
||||
"next-sitemap": "^3.1.21",
|
||||
"postcss": "^8.4.16",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.54.4",
|
||||
"sass": "^1.54.5",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && !isEmpty(initialData)) {
|
||||
|
||||
@ -3,7 +3,6 @@ import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@ -55,22 +54,14 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
||||
};
|
||||
|
||||
const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) dispatch(setResume(initialData));
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
}, [router, locale]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import '@/styles/globals.scss';
|
||||
|
||||
import env from '@beam-australia/react-env';
|
||||
import DayjsAdapter from '@date-io/dayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import Script from 'next/script';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
@ -34,27 +35,27 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
<ReduxProvider store={store}>
|
||||
<LocalizationProvider dateAdapter={DayjsAdapter}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WrapperRegistry>
|
||||
<Loading />
|
||||
<GoogleOAuthProvider clientId={env('GOOGLE_CLIENT_ID')}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WrapperRegistry>
|
||||
<Loading />
|
||||
|
||||
<Component {...pageProps} />
|
||||
<Component {...pageProps} />
|
||||
|
||||
<ModalWrapper />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
className: 'toast',
|
||||
}}
|
||||
/>
|
||||
</WrapperRegistry>
|
||||
</QueryClientProvider>
|
||||
<ModalWrapper />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
className: 'toast',
|
||||
}}
|
||||
/>
|
||||
</WrapperRegistry>
|
||||
</QueryClientProvider>
|
||||
</GoogleOAuthProvider>
|
||||
</PersistGate>
|
||||
</LocalizationProvider>
|
||||
</ReduxProvider>
|
||||
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,7 +4,9 @@ import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ActionCreators } from 'redux-undo';
|
||||
|
||||
import ResumeCard from '@/components/dashboard/ResumeCard';
|
||||
import ResumePreview from '@/components/dashboard/ResumePreview';
|
||||
@ -12,6 +14,7 @@ import Avatar from '@/components/shared/Avatar';
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
import { fetchResumes } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import styles from '@/styles/pages/Dashboard.module.scss';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
@ -25,8 +28,14 @@ export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
const Dashboard: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data } = useQuery(RESUMES_QUERY, fetchResumes);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ActionCreators.clearHistory());
|
||||
}, []);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextPage } from 'next';
|
||||
|
||||
const PrivacyPolicy: NextPage = () => (
|
||||
<div className="mx-auto my-12 prose dark:prose-invert">
|
||||
<div className="prose mx-auto my-12 dark:prose-invert">
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextPage } from 'next';
|
||||
|
||||
const TermsOfService: NextPage = () => (
|
||||
<div className="mx-auto my-12 prose dark:prose-invert">
|
||||
<div className="prose mx-auto my-12 dark:prose-invert">
|
||||
<h1>Terms of Service</h1>
|
||||
|
||||
<p>
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "تسجيل الدخول",
|
||||
"google": "تسجيل الدخول باستخدام حساب جوجل"
|
||||
"login": "تسجيل الدخول"
|
||||
},
|
||||
"body": "يرجى إدخال اسم المستخدم وكلمة المرور المرتبطين بحسابك لتسجيل الدخول والوصول إلى السير الذاتية وإدارتها ومشاركتها.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Вход",
|
||||
"google": "Вход чрез Google"
|
||||
"login": "Вход"
|
||||
},
|
||||
"body": "Моля, въведете вашето потребителско име и парола, свързани с вашия акаунт, за да влезете и получите достъп, управлявате и споделяте вашите автобиографии.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "লগইন",
|
||||
"google": "গুগল দিয়ে লগইন করুন"
|
||||
"login": "লগইন"
|
||||
},
|
||||
"body": "লগইন এবং অ্যাক্সেস, পরিচালনা এবং আপনার জীবনবৃত্তান্ত শেয়ার করতে আপনার অ্যাকাউন্টের সাথে যুক্ত আপনার ব্যবহারকারীর নাম এবং পাসওয়ার্ড লিখুন।",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Přihlásit se",
|
||||
"google": "Přihlaste se pomocí Google"
|
||||
"login": "Přihlásit se"
|
||||
},
|
||||
"body": "Zadejte prosím své uživatelské jméno a heslo spojené s vaším účtem, abyste se mohli přihlásit a získat přístup, spravovat a sdílet své životopisy.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Log ind",
|
||||
"google": "Log ind med Google"
|
||||
"login": "Log ind"
|
||||
},
|
||||
"body": "Indtast venligst dit brugernavn og din adgangskode knyttet til din konto for at logge ind og få adgang til, administrere og dele dine CV'er.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Anmeldung",
|
||||
"google": "Mit Google anmelden"
|
||||
"login": "Anmeldung"
|
||||
},
|
||||
"body": "Bitte geben Sie Ihren Benutzernamen und Ihr Passwort ein, um sich anzumelden und zuzugreifen, Ihre Bewerbungen zu verwalten und weiterzugeben.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Σύνδεση",
|
||||
"google": "Συνδεθείτε με το Google"
|
||||
"login": "Σύνδεση"
|
||||
},
|
||||
"body": "Εισαγάγετε το όνομα χρήστη και τον κωδικό πρόσβασης που σχετίζονται με τον λογαριασμό σας για να συνδεθείτε και να αποκτήσετε πρόσβαση, να διαχειριστείτε και να μοιραστείτε τα βιογραφικά σας.",
|
||||
"form": {
|
||||
|
||||
@ -84,7 +84,9 @@
|
||||
"toggle-page-break-line": "Toggle Page Break Line",
|
||||
"toggle-sidebars": "Toggle Sidebars",
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out"
|
||||
"zoom-out": "Zoom Out",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Login",
|
||||
"google": "Login with Google"
|
||||
"login": "Login"
|
||||
},
|
||||
"body": "Please enter your username and password associated with your account to login and access, manage and share your resumes.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Acceso",
|
||||
"google": "Iniciar sesión con Google"
|
||||
"login": "Acceso"
|
||||
},
|
||||
"body": "Por favor, introduzca su nombre de usuario y contraseña asociados a su cuenta para iniciar sesión y acceder, gestionar y compartir sus currículos.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "ورود",
|
||||
"google": "ورود با گوگل"
|
||||
"login": "ورود"
|
||||
},
|
||||
"body": "لطفا نام کاربری و رمز عبور مرتبط با حساب کاربری خود را وارد کنید تا وارد شوید و به رزومه خود دسترسی پیدا کنید، آن را مدیریت کنید و به اشتراک بگذارید.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Kirjaudu sisään",
|
||||
"google": "Kirjaudu Google-tilillä"
|
||||
"login": "Kirjaudu sisään"
|
||||
},
|
||||
"body": "Anna tiliisi liitetty käyttäjätunnuksesi ja salasanasi kirjautuaksesi sisään ja käyttääksesi, hallitaksesi ja jakaaksesi ansioluettelosi.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Connexion",
|
||||
"google": "Connexion avec Google"
|
||||
"login": "Connexion"
|
||||
},
|
||||
"body": "Veuillez entrer votre nom d'utilisateur et votre mot de passe associés à votre compte pour vous connecter et accéder, gérer et partager vos CVs.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "התחברות",
|
||||
"google": "התחבר עם גוגל"
|
||||
"login": "התחברות"
|
||||
},
|
||||
"body": "אנא הזן את שם המשתמש והסיסמה המשויכים לחשבונך כדי להתחבר ולגשת, לנהל ולשתף את קורות החיים שלך.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "लॉग इन करें",
|
||||
"google": "Google के साथ लॉगिन करें"
|
||||
"login": "लॉग इन करें"
|
||||
},
|
||||
"body": "लॉग इन करने और अपने रिज्यूमे को एक्सेस करने, प्रबंधित करने और साझा करने के लिए कृपया अपने खाते से संबद्ध अपना उपयोगकर्ता नाम और पासवर्ड दर्ज करें।",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Belépés",
|
||||
"google": "Jelentkezzen be a Google-lal"
|
||||
"login": "Belépés"
|
||||
},
|
||||
"body": "Kérjük, adja meg a fiókjához tartozó felhasználónevét és jelszavát a bejelentkezéshez, valamint önéletrajzainak eléréséhez, kezeléséhez és megosztásához.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Masuk",
|
||||
"google": "Masuk dengan Google"
|
||||
"login": "Masuk"
|
||||
},
|
||||
"body": "Silakan masukkan nama pengguna dan kata sandi Anda yang terkait dengan akun Anda untuk masuk dan mengakses, mengelola, dan membagikan resume Anda.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Accedi",
|
||||
"google": "Accedi con Google"
|
||||
"login": "Accedi"
|
||||
},
|
||||
"body": "Per accedere e iniziare a lavorare sui tuoi curriculum, inserisci il nome utente e la password associati al tuo account.",
|
||||
"form": {
|
||||
|
||||
29
client/public/locales/km/common.json
Normal file
29
client/public/locales/km/common.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"avatar": {
|
||||
"menu": {
|
||||
"greeting": "សួស្តី",
|
||||
"logout": "ចាកចេញ"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"credit": "គម្រោងបង្កើតឡើងដោយលោក <1> Amruth Pillai</1>",
|
||||
"license": "ដោយសហគមន៍ ដើម្បីសហគមន៍។"
|
||||
},
|
||||
"markdown": {
|
||||
"help-text": "ផ្នែកនេះគាំទ្រទម្រង់ <1> markdown</1> ។"
|
||||
},
|
||||
"date": {
|
||||
"present": "បច្ចុប្បន្ន"
|
||||
},
|
||||
"subtitle": "បង្កើតជីវត្តប្រវត្តិដោយចំហ និងមិនគិតថ្លៃ។",
|
||||
"title": "Reactive Resume",
|
||||
"toast": {
|
||||
"error": {
|
||||
"upload-file-size": "សូមបញ្ចូលតែឯកសារណាដែលមានទំហំមិនលើសពី២មេហ្គាបៃ។",
|
||||
"upload-photo-size": "សូមបញ្ចូលតែឯកសារណាដែលមានទំហំមិនលើសពី២មេហ្គាបៃ កាន់តែល្អប្រសិនបើរូបរាងជាការ៉េ។"
|
||||
},
|
||||
"success": {
|
||||
"resume-link-copied": "តំណភ្ជាប់ទៅកាន់ប្រវត្តិរូបរបស់អ្នកត្រូវបានចម្លងរួចរាល់។"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
client/public/locales/km/dashboard.json
Normal file
25
client/public/locales/km/dashboard.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"create-resume": {
|
||||
"subtitle": "ចាប់ផ្តើមពីដំបូង",
|
||||
"title": "បង្កើតប្រវត្តិរូបសង្ខេបថ្មី។"
|
||||
},
|
||||
"import-external": {
|
||||
"subtitle": "LinkedIn, JSON Resume, Reactive Resume",
|
||||
"title": "នាំចូលពីប្រភពខាងក្រៅ"
|
||||
},
|
||||
"resume": {
|
||||
"menu": {
|
||||
"delete": "លុប",
|
||||
"duplicate": "ស្ទួន",
|
||||
"open": "បើក",
|
||||
"rename": "ប្ដូរឈ្មោះ",
|
||||
"share-link": "ចែករំលែកតំណ",
|
||||
"tooltips": {
|
||||
"delete": "តើអ្នកប្រាកដទេដែលចង់លុបវាចេញ? បញ្ជាក់ៈ ក្រោយពីលុបហើយ មិនអាចទាញយកមកវិញបានទេ។",
|
||||
"share-link": "អ្នកត្រូវផ្លាស់ប្តូរការមើលឃើញនៃប្រវត្តិរូបរបស់អ្នកជាសាធារណៈ ដើម្បីឱ្យអ្នកដទៃមើលឃើញ។"
|
||||
}
|
||||
},
|
||||
"timestamp": "បានធ្វើបច្ចុប្បន្នភាពចុងក្រោយ {{timestamp}} មុន។"
|
||||
},
|
||||
"title": "ផ្ទាំងគ្រប់គ្រង"
|
||||
}
|
||||
41
client/public/locales/km/landing.json
Normal file
41
client/public/locales/km/landing.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"actions": {
|
||||
"app": "ចូលទៅកាន់កម្មវិធី",
|
||||
"login": "ចូល",
|
||||
"logout": "ចាកចេញ",
|
||||
"register": "ចុះឈ្មោះ"
|
||||
},
|
||||
"features": {
|
||||
"heading": "មុខងារ",
|
||||
"list": {
|
||||
"ads": "គ្មានការផ្សាយពាណិជ្ជកម្ម",
|
||||
"export": "នាំចេញប្រវត្តិរូបសង្ខេបរបស់អ្នកទៅជាទម្រង់ JSON ឬ PDF",
|
||||
"free": "ឥតគិតថ្លៃជារៀងរហូត",
|
||||
"import": "នាំចូលទិន្នន័យពី LinkedIn, JSON Resume",
|
||||
"languages": "អាចចូលប្រើបានច្រើនភាសា",
|
||||
"more": "និងមុខងារពិសេសៗជាច្រើនទៀត <1>អានបន្ថែមនៅទីនេះ</1>",
|
||||
"tracking": "គ្មានការតាមដានអ្នកប្រើប្រាស់"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"heading": "តំណ",
|
||||
"links": {
|
||||
"donate": "បរិច្ចាគ",
|
||||
"github": "ប្រភពកូដ",
|
||||
"privacy": "គោលការណ៍ភាពឯកជន",
|
||||
"service": "លក្ខខណ្ឌនៃសេវាកម្ម"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "រូបថតអេក្រង់"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "សក្ខីកម្ម",
|
||||
"body": "ល្អ ឬមិនល្អ ខ្ញុំចង់រីករាយនឹងទទួលមតិរបស់អ្នកលើ Reactive Resume និងបទពិសោធន៍ដែលអ្នកទទួលបាន។ <br/> នេះគឺជាមតិតដែលយើងទទួលបានពីអ្នកប្រើប្រាស់ពីជុំវិញពិភពលោក។",
|
||||
"contact": "អ្នកអាចទាក់ទងមកខ្ញុំតាមរយៈ <1>អ៊ីមែលរបស់ខ្ញុំ</1> ឬតាមរយៈទម្រង់ទំនាក់ទំនងនៅលើ <3>គេហទំព័ររបស់ខ្ញុំ</3> ."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume គឺជាកម្មវិធីបង្កើតប្រវត្តិរូបសង្ខេបដោយឥតគិតថ្លៃ និងបើកចំហ ដែលត្រូវបានបង្កើតឡើងដើម្បីឲ្យលោកអ្នកអាចបង្កើត កែប្រែ ក៏ដូចជាចែករំលែកនូវប្រវត្តិរូបសង្ខេបរបស់អ្នកបានយ៉ាងងាយស្រួលដូចជា 1,2,3។ អ្នកអាចបង្កើតប្រវត្តិរូបបានច្រើន ចែករំលែកទៅកាន់អ្នកដែលកំពុងស្វែងរកបុគ្គលិក ឬមិត្តភក្តិតាមរយៈតំណភ្ជាប់ ហើយក៏អាចព្រីនជា PDF, មិនគិតថ្លៃ គ្មានការផ្សាយណាណិជ្ជកម្ម គ្មានការតាមដានឬលួចទិន្នន័យ ត្រឹមត្រូវ និងរក្សារបាននូវឯកជនភាព។",
|
||||
"heading": "សង្ខេប"
|
||||
}
|
||||
}
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "ಲಾಗಿನ್ ಮಾಡಿ",
|
||||
"google": "ಗೂಗಲ್ ನೊಂದಿಗೆ ಲಾಗಿನ್ ಮಾಡಿ"
|
||||
"login": "ಲಾಗಿನ್ ಮಾಡಿ"
|
||||
},
|
||||
"body": "ಲಾಗಿನ್ ಮಾಡಲು ಮತ್ತು ಪ್ರವೇಶಿಸಲು, ನಿಮ್ಮ ರೇಸುಮೆಗಳನ್ನು ನಿರ್ವಹಿಸಲು ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಲು ದಯವಿಟ್ಟು ನಿಮ್ಮ ಬಳಕೆದಾರಹೆಸರು ಮತ್ತು ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ನಮೂದಿಸಿ.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "ലോഗിൻ",
|
||||
"google": "Google ഉപയോഗിച്ച് ലോഗിൻ ചെയ്യുക"
|
||||
"login": "ലോഗിൻ"
|
||||
},
|
||||
"body": "ലോഗിൻ ചെയ്യാനും ആക്സസ് ചെയ്യാനും നിങ്ങളുടെ റെസ്യൂമെകൾ നിയന്ത്രിക്കാനും പങ്കിടാനും നിങ്ങളുടെ അക്കൗണ്ടുമായി ബന്ധപ്പെട്ട ഉപയോക്തൃനാമവും പാസ്വേഡും നൽകുക.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Inloggen",
|
||||
"google": "Inloggen met Google"
|
||||
"login": "Inloggen"
|
||||
},
|
||||
"body": "Voer jouw gebruikersnaam en wachtwoord in die bij jouw account horen om in te loggen en jouw cv's te openen, beheren en delen.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "ଲଗ ଇନ",
|
||||
"google": "ଗୁଗୁଲ୍ ସହିତ ଲଗ୍ଇନ୍ କରନ୍ତୁ"
|
||||
"login": "ଲଗ ଇନ"
|
||||
},
|
||||
"body": "ଲଗ୍ଇନ୍ ଏବଂ ଆକ୍ସେସ୍, ପରିଚାଳନା ଏବଂ ଅଂଶୀଦାର କରିବା ପାଇଁ ଦୟାକରି ଆପଣଙ୍କର ଖାତା ସହିତ ଜଡିତ ଆପଣଙ୍କର ଉପଯୋଗକର୍ତ୍ତା ନାମ ଏବଂ ପାସୱାର୍ଡ ପ୍ରବେଶ କରନ୍ତୁ ।",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Login",
|
||||
"google": "Zaloguj się za pomocą Google"
|
||||
"login": "Login"
|
||||
},
|
||||
"body": "Podaj nazwę użytkownika i hasło przypisane do Twojego konta, aby zalogować się i uzyskać dostęp do swoich życiorysów, zarządzać nimi i udostępniać je.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Entrar",
|
||||
"google": "Entrar com o Google"
|
||||
"login": "Entrar"
|
||||
},
|
||||
"body": "Por favor, digite seu nome de usuário e senha associados à sua conta para fazer login e acessar, gerenciar e compartilhar seus currículos.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Войти",
|
||||
"google": "Войти через Google"
|
||||
"login": "Войти"
|
||||
},
|
||||
"body": "Пожалуйста, введите имя пользователя и пароль, связанные с вашей учетной записью, чтобы войти в систему и получить доступ, управлять и делиться своими резюме.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Logga in",
|
||||
"google": "Logga in med Google"
|
||||
"login": "Logga in"
|
||||
},
|
||||
"body": "Vänligen ange ditt användarnamn och lösenord som är kopplat till ditt konto för att logga in och komma åt, hantera och dela dina CV.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "உள்நுழைய",
|
||||
"google": "Google மூலம் உள்நுழைக"
|
||||
"login": "உள்நுழைய"
|
||||
},
|
||||
"body": "உள்நுழைவதற்கும் அணுகுவதற்கும், உங்கள் பயோடேட்டாக்களை நிர்வகிப்பதற்கும் பகிரவும் உங்கள் கணக்குடன் தொடர்புடைய உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லை உள்ளிடவும்.",
|
||||
"form": {
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
"share-link": "Bağlantıyı Paylaş",
|
||||
"tooltips": {
|
||||
"delete": "Bu özgeçmişi silmek istediğinizden emin misiniz? Bu geri dönüşü olmayan bir eylemdir.",
|
||||
"share-link": "Başkalarına görünür kılmak için özgeçmişinizin görünürlüğünü herkese açık olarak değiştirmeniz gerekir."
|
||||
"share-link": "Özgeçmişinizin herkese açık bir şekilde görüntülenebilmesi için, görünürlük ayarını değiştirmeniz gerekmektedir."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Oturum Aç",
|
||||
"google": "Google ile Giriş yap"
|
||||
"login": "Oturum Aç"
|
||||
},
|
||||
"body": "Giriş yapmak ve özgeçmişlerinize erişmek, yönetmek ve paylaşmak için lütfen hesabınızla ilişkili kullanıcı adınızı ve şifrenizi girin.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "Đăng nhập",
|
||||
"google": "Đăng nhập với Google"
|
||||
"login": "Đăng nhập"
|
||||
},
|
||||
"body": "Vui lòng nhập tên người dùng và mật khẩu được liên kết với tài khoản của bạn để đăng nhập và truy cập, quản lý và chia sẻ hồ sơ của bạn.",
|
||||
"form": {
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "登录",
|
||||
"google": "使用 Google 帐号登录"
|
||||
"login": "登录"
|
||||
},
|
||||
"body": "请输入与您的帐户关联的用户名和密码,以登录和访问、管理和分享您的简历。",
|
||||
"form": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user