mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 12:32:28 +10:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b24da90ba7 | |||
| 2aa7dbd3ad | |||
| 9f8f2c4b8b | |||
| 5331ecccc1 | |||
| f2ec86940c | |||
| cd74e707ba | |||
| ff101dbfac | |||
| 5024c19f87 | |||
| c9850b5815 | |||
| 6fe4e7d7e1 | |||
| a5b8b91e82 | |||
| cc7095adc3 | |||
| e2703f55aa | |||
| 8c5849c988 | |||
| 5322ab2420 | |||
| b84e6bcfb1 | |||
| a4ab0174c7 | |||
| 7e93b5a757 | |||
| 044820fa71 | |||
| 322178e8a4 | |||
| 6358fbad30 | |||
| d342c0a9af | |||
| 63084eebb4 | |||
| 3b4ea00db8 | |||
| c8f7bffe7e | |||
| 3ff56f89d9 | |||
| 7fb9f27837 | |||
| c9685d4ce7 | |||
| 4dc987e27d | |||
| f7af06ae9a | |||
| a5c337faa3 | |||
| fc4704f0a6 | |||
| d968334ada | |||
| fea6d23178 | |||
| 3fefc95572 | |||
| b07e7d1213 | |||
| d47b8bfb03 | |||
| 5bf7fbdae1 | |||
| fca766b382 | |||
| feadfb1b67 | |||
| e69000f221 | |||
| 6b4a54465a | |||
| 878659999f | |||
| 1868c47e30 | |||
| 51442efc23 | |||
| 556e962ec5 | |||
| b5ce67f863 | |||
| c3ce89dc3a | |||
| e87930c758 | |||
| 815a693e58 | |||
| 8287fcae96 | |||
| cd7fe6c404 | |||
| d47d5dd819 | |||
| 1919d79e43 | |||
| ab08cd9e34 | |||
| 2522bdd0a2 | |||
| f9b6aefffe | |||
| 2ba6658a0b | |||
| dbc46f27a3 | |||
| f21e1caed1 | |||
| 4ffe2a6330 | |||
| 1bc0438872 | |||
| 57fb9fdaea | |||
| 58ce641f18 | |||
| 5f4e7802e4 | |||
| 42d3109ae1 | |||
| f7ca7b97fa | |||
| f5d8a54134 | |||
| eaec14dc62 | |||
| c93b3264cd | |||
| bf41aa9c6c | |||
| 8af6bfd5ae | |||
| ab08c10874 | |||
| 9af9a0284e | |||
| 716a05032d | |||
| 43e43e7d76 | |||
| c91af3668d | |||
| 52f41f0b3b | |||
| 3b709d606b | |||
| 2e5fafac62 | |||
| ea2aee2d25 | |||
| e36fbb5f64 | |||
| 5221ef707b | |||
| f0df806f01 | |||
| 9d01d6a833 | |||
| 1914ebb9ae | |||
| 686dba90c9 | |||
| 95dc3bf571 | |||
| 1c8fdbf848 | |||
| d8357c9959 | |||
| 90e994377b | |||
| 82c6ee6d5d | |||
| 7b615e73c3 | |||
| 268e4a87fe | |||
| 73f8eb84c9 | |||
| a31ef89996 | |||
| d6bca7ebab | |||
| e0a42fd928 | |||
| deb4e0a0de | |||
| a687062866 | |||
| 700439c8a8 | |||
| fb09283e53 | |||
| 88ac365e03 | |||
| aec78cf875 | |||
| 77c587681b | |||
| 7ac8b906d9 | |||
| e9a5f86a6a | |||
| 7238a3b50e | |||
| ebe13fa82e | |||
| 6ee290a625 | |||
| 69f2b7070f | |||
| 11bea1c7c4 | |||
| 68a1dc65c1 | |||
| 4b1ce539d5 | |||
| a6fbb8191d | |||
| 552ff281b8 | |||
| 54fad2f6d8 | |||
| 78edcd7d0e | |||
| a8034b21d5 | |||
| f0e95905d2 | |||
| 69a5276614 | |||
| 2e62eea351 | |||
| 13d972b8f3 | |||
| 03cb198e95 | |||
| 67ee55b502 | |||
| b5998d7f3a | |||
| f71cf99b77 | |||
| a2092a6a39 | |||
| 43c09666a0 | |||
| 0da23f95fd | |||
| e8f44e2142 | |||
| fbb237e982 | |||
| 7f7c1d7b87 | |||
| be0b7f20f9 | |||
| 0672988fff | |||
| 75dad60cb5 | |||
| 0140e3fce0 | |||
| 42d0e14b98 | |||
| 9a42d684fb | |||
| ab6ad65445 | |||
| b613764ccc | |||
| ac44d0489f | |||
| c57e6fbbb8 | |||
| 6c6da215c8 | |||
| be700c7629 | |||
| b697f73492 | |||
| 3106f94989 | |||
| 50f41f73d5 | |||
| 83e3f59e68 | |||
| 056c61e985 | |||
| d1a1b68302 | |||
| 6bd7b9a50f | |||
| e6967aab88 | |||
| 47e96803e3 | |||
| f9ef4d0a64 | |||
| c4b4e6013f | |||
| 24bbc46c32 | |||
| 85bc9ef124 | |||
| 33755a8573 | |||
| ab45321889 | |||
| 940b310f64 |
@ -2,9 +2,9 @@
|
||||
/app
|
||||
|
||||
# Build Artifacts
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
**/.turbo
|
||||
/server/dist
|
||||
/client/.next
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
@ -19,7 +19,7 @@ CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
|
||||
10
.env.example
10
.env.example
@ -1,7 +1,3 @@
|
||||
# Turbo Cache (Optional)
|
||||
TURBO_TEAM=
|
||||
TURBO_TOKEN=
|
||||
|
||||
# Server + Client
|
||||
TZ=UTC
|
||||
PUBLIC_URL=http://localhost:3000
|
||||
@ -15,7 +11,7 @@ POSTGRES_PASSWORD=postgres
|
||||
|
||||
# Server
|
||||
SECRET_KEY=
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_SSL_CERT=
|
||||
JWT_SECRET=
|
||||
@ -34,7 +30,7 @@ STORAGE_ENDPOINT=
|
||||
STORAGE_URL_PREFIX=
|
||||
STORAGE_ACCESS_KEY=
|
||||
STORAGE_SECRET_KEY=
|
||||
PDF_DELETION_TIME=
|
||||
PDF_DELETION_TIME=345600000
|
||||
|
||||
# Flags (Client)
|
||||
# Client
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
@ -14,6 +14,7 @@
|
||||
// TypeScript ESLint
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
|
||||
13
.github/workflows/digitalocean-deploy.yml
vendored
13
.github/workflows/digitalocean-deploy.yml
vendored
@ -8,14 +8,21 @@ on:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
on-success:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Install DigitalOcean CLI
|
||||
uses: digitalocean/action-doctl@v2.2.0
|
||||
uses: digitalocean/action-doctl@v2.3.0
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||
|
||||
- name: Create Deployment with Latest Version
|
||||
run: doctl apps create-deployment ${{ secrets.DIGITALOCEAN_APP_ID }} --wait --force-rebuild
|
||||
|
||||
on-failure:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||
steps:
|
||||
- name: Abruptly end the worklfow
|
||||
run: exit 1
|
||||
|
||||
92
.github/workflows/docker-build-push.yml
vendored
92
.github/workflows/docker-build-push.yml
vendored
@ -1,36 +1,35 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: Client
|
||||
build_matrix:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
strategy:
|
||||
matrix:
|
||||
image: [client, server]
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.1.0
|
||||
with:
|
||||
fetch-depth: 2
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
name: App Version
|
||||
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: amd64
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
@ -45,68 +44,15 @@ jobs:
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Client Image
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v3.3.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
file: ${{ matrix.image }}/Dockerfile
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ env.version }}
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ env.version }}
|
||||
|
||||
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.1.0
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Server Image
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ env.version }}
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ env.version }}
|
||||
amruthpillai/reactive-resume:${{ matrix.image }}-latest
|
||||
amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
|
||||
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.gitpod
|
||||
!.env.example
|
||||
|
||||
# Project Dependencies
|
||||
|
||||
41
.gitpod.yml
Normal file
41
.gitpod.yml
Normal file
@ -0,0 +1,41 @@
|
||||
tasks:
|
||||
- name: Run PostgreSQL Database
|
||||
command: docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
|
||||
|
||||
- name: Install Project Dependencies
|
||||
command: |
|
||||
pnpm install
|
||||
pnpm dlx playwright install --with-deps chromium
|
||||
gp sync-done deps
|
||||
|
||||
- name: Generate Environment Variables
|
||||
init: gp sync-await deps
|
||||
command: |
|
||||
if [ -f .env ]; then
|
||||
echo "Found .env in workspace, skipping generation"
|
||||
else
|
||||
pnpm generate-env
|
||||
fi
|
||||
gp sync-done env
|
||||
|
||||
- name: Build and Run Project
|
||||
init: gp sync-await env
|
||||
command: |
|
||||
pnpm build
|
||||
pnpm start
|
||||
|
||||
ports:
|
||||
# PostgreSQL
|
||||
- port: 5432
|
||||
onOpen: ignore
|
||||
visibility: private
|
||||
|
||||
# Client
|
||||
- port: 3100
|
||||
onOpen: ignore
|
||||
visibility: public
|
||||
|
||||
# Client
|
||||
- port: 3000
|
||||
onOpen: open-browser
|
||||
visibility: public
|
||||
17
README.md
17
README.md
@ -6,7 +6,8 @@
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE)
|
||||
[](https://translate.rxresu.me)
|
||||
[](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||

|
||||

|
||||
[](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
||||
|
||||
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
||||
@ -18,7 +19,7 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
## Table of Contents
|
||||
|
||||
- [Reactive Resume](#reactive-resume)
|
||||
- [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
|
||||
- [Go to App | Docs](#go-to-app--docs)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Languages](#languages)
|
||||
@ -93,7 +94,7 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
- Swedish (Svenska)
|
||||
- Tamil (தமிழ்)
|
||||
- Turkish (Türkçe)
|
||||
- Ukranian (Українська мова)
|
||||
- Ukrainian (Українська мова)
|
||||
- Vietnamese (Tiếng Việt)
|
||||
|
||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||
@ -104,7 +105,11 @@ The docs include an extensive [Tutorial](https://docs.rxresu.me/tutorial) sectio
|
||||
|
||||
## Build from Source
|
||||
|
||||
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.
|
||||
[](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
|
||||
|
||||
Initially building the image and project on Gitpod will take at least ~10 minutes, so please be patient on first launch.
|
||||
|
||||
For extensive information on how to build the app on your local machine, head over to the docs [Source Code](https://docs.rxresu.me/source-code) section.
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -129,6 +134,10 @@ Reactive Resume would be nothing without the folks who supported me and kept the
|
||||
### [GitHub Sponsor](https://github.com/sponsors/AmruthPillai)
|
||||
### [PayPal](https://paypal.me/RajaRajanA)
|
||||
|
||||
## GitHub Star History
|
||||
|
||||
[](https://star-history.com/#AmruthPillai/Reactive-Resume&Date)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- [Next.js](https://nextjs.org/), frontend
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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.7.20' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
@ -21,12 +21,6 @@ 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
|
||||
|
||||
ARG TURBO_TEAM
|
||||
ARG TURBO_TOKEN
|
||||
|
||||
ENV TURBO_TEAM $TURBO_TEAM
|
||||
ENV TURBO_TOKEN $TURBO_TOKEN
|
||||
|
||||
RUN pnpm run build --filter client
|
||||
|
||||
FROM base as production
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@apply h-full w-full #{!important};
|
||||
@apply h-full w-full overflow-visible #{!important};
|
||||
}
|
||||
|
||||
.artboard {
|
||||
|
||||
@ -53,13 +53,12 @@ const Header = () => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -36,13 +36,6 @@
|
||||
top: calc(279mm - 19px);
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
text-indent: -1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pageNumber {
|
||||
|
||||
@ -62,7 +62,7 @@ const LeftSidebar = () => {
|
||||
for (const item of left) {
|
||||
const id = (item as any).id;
|
||||
const component = (item as any).component;
|
||||
const type = component.props.type || 'basic';
|
||||
const type = component.props.type;
|
||||
const addMore = !!component.props.addMore;
|
||||
|
||||
sectionsComponents.push(
|
||||
@ -108,7 +108,7 @@ const LeftSidebar = () => {
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<nav className="overflow-y-scroll">
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Link href="/dashboard">
|
||||
<Logo size={40} />
|
||||
@ -142,9 +142,10 @@ const LeftSidebar = () => {
|
||||
|
||||
<main>
|
||||
{sectionsList()}
|
||||
|
||||
{customSections.map(({ id }) => (
|
||||
<section key={id} id={`section-${id}`}>
|
||||
<Section path={`sections.${id}`} isEditable isHideable isDeletable />
|
||||
<Section path={`sections.${id}`} type="custom" isEditable isHideable isDeletable />
|
||||
</section>
|
||||
))}
|
||||
|
||||
|
||||
@ -47,12 +47,8 @@ const Section: React.FC<Props> = ({
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const handleAdd = () => {
|
||||
const id = path.split('.')[1];
|
||||
let modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
const modal: ModalName = `builder.sections.${type}`;
|
||||
|
||||
if (type) {
|
||||
modal = `builder.sections.${type}`;
|
||||
}
|
||||
dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
|
||||
};
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ const RightSidebar = () => {
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<nav className="overflow-y-scroll">
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Avatar size={40} />
|
||||
<Divider />
|
||||
|
||||
@ -3,7 +3,7 @@ import { Button } from '@mui/material';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL } from '@/constants/index';
|
||||
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL, REDDIT_URL } from '@/constants/index';
|
||||
|
||||
import styles from './Links.module.scss';
|
||||
|
||||
@ -50,6 +50,12 @@ const Links = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.reddit')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DOCS_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.docs')}
|
||||
|
||||
@ -51,7 +51,7 @@ const Settings = () => {
|
||||
const pageConfig: PageConfig | undefined = useMemo(() => get(resume, 'metadata.page'), [resume]);
|
||||
|
||||
const isDarkMode = useMemo(() => theme === 'dark', [theme]);
|
||||
const exampleDateString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const exampleDateString = useMemo(() => `Eg. ${dayjs().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const themeString = useMemo(() => (isDarkMode ? 'Matte Black Everything' : 'As bright as your future'), [isDarkMode]);
|
||||
|
||||
const { mutateAsync: loadSampleDataMutation } = useMutation<Resume, ServerError, LoadSampleDataParams>(
|
||||
|
||||
@ -6,15 +6,17 @@ import { useState } from 'react';
|
||||
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import getGravatarUrl from '@/utils/getGravatarUrl';
|
||||
|
||||
import styles from './Avatar.module.scss';
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
interactive?: boolean;
|
||||
};
|
||||
|
||||
const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
const Avatar: React.FC<Props> = ({ size = 64, interactive = true }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@ -34,6 +36,11 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleOpenProfile = () => {
|
||||
dispatch(setModalState({ modal: 'auth.profile', state: { open: true } }));
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout());
|
||||
handleClose();
|
||||
@ -43,7 +50,7 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<IconButton onClick={handleOpen} disabled={!interactive}>
|
||||
<Image
|
||||
width={size}
|
||||
height={size}
|
||||
@ -54,9 +61,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
</IconButton>
|
||||
|
||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||
<MenuItem>
|
||||
<MenuItem onClick={handleOpenProfile}>
|
||||
<div>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')},</span>
|
||||
<p>{user?.name}</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
import { isEmpty } from 'lodash';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
type Props = {
|
||||
children?: string;
|
||||
@ -12,7 +14,11 @@ const Markdown: React.FC<Props> = ({ className, children }) => {
|
||||
if (!children || isEmpty(children)) return null;
|
||||
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} className={clsx('markdown', className)}>
|
||||
<ReactMarkdown
|
||||
className={clsx('markdown', className)}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
@ -22,6 +22,7 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
|
||||
const dateFormat = useAppSelector((state) => state.resume.present.metadata.date.format);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(stateValue);
|
||||
@ -56,14 +57,16 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
||||
if (type === 'date') {
|
||||
return (
|
||||
<DatePicker
|
||||
showToolbar
|
||||
openTo="year"
|
||||
label={label}
|
||||
value={value}
|
||||
toolbarFormat={dateFormat}
|
||||
views={['year', 'month', 'day']}
|
||||
renderInput={(params) => <TextField {...params} error={false} className={className} />}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && onChangeValue('');
|
||||
date && dayjs(date).utc().isValid() && onChangeValue(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && onChangeValue(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@ export const languages: Language[] = [
|
||||
{ code: 'ca', name: 'Catalan', localName: 'Valencian' },
|
||||
{ code: 'cs', name: 'Czech', localName: 'čeština' },
|
||||
{ code: 'da', name: 'Danish', localName: 'Dansk' },
|
||||
{ code: 'de', name: 'German', localName: 'Deutsch' },
|
||||
{ code: 'de', name: 'German', localName: 'Deutsch Formell / Sie' },
|
||||
{ code: 'el', name: 'Greek', localName: 'Ελληνικά' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'es', name: 'Spanish', localName: 'Español' },
|
||||
|
||||
@ -13,6 +13,11 @@ export const DOCS_URL = 'https://docs.rxresu.me';
|
||||
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
|
||||
export const TRANSLATE_URL = 'https://translate.rxresu.me/';
|
||||
export const DIGITALOCEAN_URL = 'https://pillai.xyz/digitalocean';
|
||||
export const REDDIT_URL = 'https://www.reddit.com/r/reactiveresume/';
|
||||
export const GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
|
||||
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
|
||||
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';
|
||||
|
||||
// Default Error Message
|
||||
export const DEFAULT_ERROR_MESSAGE =
|
||||
'Something went wrong while performing this action, please report this issue on GitHub.';
|
||||
|
||||
@ -62,14 +62,7 @@ const LoginModal: React.FC = () => {
|
||||
};
|
||||
|
||||
const onSubmit = async ({ identifier, password }: FormData) => {
|
||||
await loginMutation(
|
||||
{ identifier, password },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
await loginMutation({ identifier, password });
|
||||
|
||||
handleClose();
|
||||
};
|
||||
@ -86,14 +79,14 @@ const LoginModal: React.FC = () => {
|
||||
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
const handleGoogleLoginError = () => {
|
||||
toast.error("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
const PasswordVisibility = (): React.ReactElement => {
|
||||
@ -117,7 +110,7 @@ const LoginModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
@ -169,7 +162,7 @@ const LoginModal: React.FC = () => {
|
||||
|
||||
<p className="text-xs">
|
||||
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
||||
In case you have forgotten your password, you can{' '}
|
||||
In case you have forgotten your password, you can
|
||||
<a onClick={handleRecoverAccount}>recover your account here.</a>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
@ -81,14 +81,14 @@ const RegisterModal: React.FC = () => {
|
||||
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
const handleGoogleLoginError = () => {
|
||||
toast("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -100,7 +100,7 @@ const RegisterModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
159
client/modals/auth/UserProfileModal.tsx
Normal file
159
client/modals/auth/UserProfileModal.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { CrisisAlert, ManageAccounts } from '@mui/icons-material';
|
||||
import { Button, Divider, TextField } from '@mui/material';
|
||||
import Joi from 'joi';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import Avatar from '@/components/shared/Avatar';
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
import { deleteAccount, updateProfile, UpdateProfileParams } from '@/services/auth';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const defaultState: FormData = {
|
||||
name: '',
|
||||
email: '',
|
||||
};
|
||||
|
||||
const schema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
email: Joi.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required(),
|
||||
});
|
||||
|
||||
const UserProfileModal = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [deleteText, setDeleteText] = useState<string>('');
|
||||
const isDeleteTextValid = useMemo(() => deleteText.toLowerCase() === 'delete', [deleteText]);
|
||||
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
const { open: isOpen } = useAppSelector((state) => state.modal['auth.profile']);
|
||||
|
||||
const { mutateAsync: deleteAccountMutation } = useMutation<void, ServerError>(deleteAccount);
|
||||
const { mutateAsync: updateProfileMutation } = useMutation<void, ServerError, UpdateProfileParams>(updateProfile);
|
||||
|
||||
const { reset, getFieldState, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
resolver: joiResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user && !getFieldState('name').isTouched && !getFieldState('email').isTouched) {
|
||||
reset({ name: user.name, email: user.email });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setModalState({ modal: 'auth.profile', state: { open: false } }));
|
||||
};
|
||||
|
||||
const handleUpdate = handleSubmit(async (data) => {
|
||||
handleClose();
|
||||
await updateProfileMutation({ name: data.name });
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteAccountMutation();
|
||||
handleClose();
|
||||
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal isOpen={isOpen} handleClose={handleClose} heading="Your Account" icon={<ManageAccounts />}>
|
||||
<div className="grid gap-4">
|
||||
<form className="grid gap-4 xl:w-2/3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar interactive={false} />
|
||||
|
||||
<div className="grid flex-1 gap-1.5">
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.auth.profile.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="pl-4 text-[10.25px] opacity-50">
|
||||
<Trans t={t} i18nKey="modals.auth.profile.form.avatar.help-text">
|
||||
You can update your profile picture on{' '}
|
||||
<a href="https://gravatar.com/" target="_blank" rel="noreferrer">
|
||||
Gravatar
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
disabled
|
||||
label={t('modals.auth.profile.form.email.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={t('modals.auth.profile.form.email.help-text')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button onClick={handleUpdate}>{t('modals.auth.profile.actions.save')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="my-2">
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<CrisisAlert />
|
||||
<h5 className="font-medium">{t('modals.auth.profile.delete-account.heading')}</h5>
|
||||
</div>
|
||||
|
||||
<p className="text-xs opacity-75">{t('modals.auth.profile.delete-account.body', { keyword: 'delete' })}</p>
|
||||
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<TextField
|
||||
value={deleteText}
|
||||
placeholder="Type 'delete' to confirm"
|
||||
onChange={(e) => setDeleteText(e.target.value)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button variant="contained" color="error" disabled={!isDeleteTextValid} onClick={handleDelete}>
|
||||
{t('modals.auth.profile.delete-account.actions.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileModal;
|
||||
@ -134,7 +134,7 @@ const AwardModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -134,7 +134,7 @@ const CertificateModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -151,7 +151,7 @@ const CustomModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -175,7 +175,7 @@ const CustomModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -173,7 +173,7 @@ const EducationModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -197,7 +197,7 @@ const EducationModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -143,7 +143,7 @@ const ProjectModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -167,7 +167,7 @@ const ProjectModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -134,7 +134,7 @@ const PublicationModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -140,7 +140,7 @@ const VolunteerModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -164,7 +164,7 @@ const VolunteerModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -140,7 +140,7 @@ const WorkModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -164,7 +164,7 @@ const WorkModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -6,7 +6,6 @@ import Joi from 'joi';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
@ -66,15 +65,10 @@ const CreateResumeModal: React.FC = () => {
|
||||
}, [name, setValue]);
|
||||
|
||||
const onSubmit = async ({ name, slug, isPublic }: FormData) => {
|
||||
try {
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
@ -68,8 +68,8 @@ const ImportExternalModal: React.FC = () => {
|
||||
}
|
||||
|
||||
await mutateAsync({ integration, file });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import ForgotPasswordModal from './auth/ForgotPasswordModal';
|
||||
import LoginModal from './auth/LoginModal';
|
||||
import RegisterModal from './auth/RegisterModal';
|
||||
import ResetPasswordModal from './auth/ResetPasswordModal';
|
||||
import UserProfileModal from './auth/UserProfileModal';
|
||||
import AwardModal from './builder/sections/AwardModal';
|
||||
import CertificateModal from './builder/sections/CertificateModal';
|
||||
import CustomModal from './builder/sections/CustomModal';
|
||||
@ -49,6 +50,7 @@ const ModalWrapper: React.FC = () => {
|
||||
<RegisterModal />
|
||||
<ForgotPasswordModal />
|
||||
<ResetPasswordModal />
|
||||
<UserProfileModal />
|
||||
|
||||
{/* Dashboard */}
|
||||
<CreateResumeModal />
|
||||
|
||||
@ -13,71 +13,71 @@
|
||||
"@emotion/css": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@hello-pangea/dnd": "^16.0.1",
|
||||
"@hello-pangea/dnd": "^16.2.0",
|
||||
"@hookform/resolvers": "2.9.10",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/lab": "^5.0.0-alpha.107",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/system": "^5.10.13",
|
||||
"@mui/x-date-pickers": "5.0.8",
|
||||
"@next/env": "^13.0.3",
|
||||
"@react-oauth/google": "^0.4.0",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
"axios": "^1.1.3",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/lab": "^5.0.0-alpha.116",
|
||||
"@mui/material": "^5.11.5",
|
||||
"@mui/system": "^5.11.5",
|
||||
"@mui/x-date-pickers": "5.0.14",
|
||||
"@react-oauth/google": "^0.6.0",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"axios": "^1.2.3",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"dayjs": "^1.11.7",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "13.0.3",
|
||||
"next-i18next": "^12.1.0",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "13.1.2",
|
||||
"next-i18next": "^13.0.3",
|
||||
"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.39.3",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-icons": "^4.6.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-query": "^3.39.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"react-zoom-pan-pinch": "^2.2.0",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.2.1",
|
||||
"redux-saga": "^1.2.2",
|
||||
"redux-undo": "^1.0.1",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.2",
|
||||
"remark-math": "^5.1.1",
|
||||
"sharp": "^0.31.3",
|
||||
"uuid": "^9.0.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/webfontloader": "^1.6.35",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint-config-next": "^13.0.3",
|
||||
"eslint-plugin-tailwindcss": "^3.6.2",
|
||||
"next-sitemap": "^3.1.31",
|
||||
"postcss": "^8.4.19",
|
||||
"sass": "^1.56.1",
|
||||
"eslint-config-next": "^13.1.2",
|
||||
"eslint-plugin-tailwindcss": "^3.8.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"next-sitemap": "^3.1.45",
|
||||
"postcss": "^8.4.21",
|
||||
"sass": "^1.57.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { DEFAULT_ERROR_MESSAGE } from '@/constants/index';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
@ -61,10 +62,12 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(resume) && router.locale !== resume.metadata.locale) {
|
||||
const locale = get(resume, 'metadata.locale', 'en');
|
||||
|
||||
if (!isEmpty(resume) && router.locale !== locale) {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale: resume.metadata.locale });
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
}
|
||||
}, [resume, router]);
|
||||
|
||||
@ -103,7 +106,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
toast.error(DEFAULT_ERROR_MESSAGE);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ type Props = {
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, QueryParams> = async ({
|
||||
query,
|
||||
locale,
|
||||
locale = 'en',
|
||||
}) => {
|
||||
const { username, slug, secretKey } = query as QueryParams;
|
||||
|
||||
@ -35,7 +35,7 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
||||
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
|
||||
|
||||
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
||||
const displayLocale = resume.metadata.locale || locale || 'en';
|
||||
const displayLocale = get(resume, 'metadata.locale') ?? locale;
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@ -21,7 +21,7 @@ import WrapperRegistry from '@/wrappers/index';
|
||||
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Reactive Resume</title>
|
||||
<title>Reactive Resume | A free and open source resume builder</title>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@ -2,7 +2,7 @@ import { NextPage } from 'next';
|
||||
import NextDocument, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
|
||||
|
||||
const Document: NextPage = () => (
|
||||
<Html>
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
|
||||
<body>
|
||||
|
||||
@ -20,7 +20,7 @@ import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL, REDDIT_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
@ -177,6 +177,12 @@ const Home: NextPage = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.reddit')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.donate')}
|
||||
|
||||
@ -11,7 +11,6 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
@ -69,7 +68,6 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({
|
||||
username: resume.user.username,
|
||||
slug: resume.slug,
|
||||
@ -77,9 +75,6 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
});
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"add": "Neue {{token}} hinzufügen",
|
||||
"delete": "Löschen {{token}}",
|
||||
"edit": "Bearbeiten {{token}}"
|
||||
"add": "{{token}} hinzufügen",
|
||||
"delete": "{{token}} löschen",
|
||||
"edit": "{{token}} bearbeiten",
|
||||
"duplicate": "Abschnitt duplizieren"
|
||||
},
|
||||
"columns": {
|
||||
"heading": "Spalten",
|
||||
@ -17,10 +18,10 @@
|
||||
"label": "Beschreibung"
|
||||
},
|
||||
"email": {
|
||||
"label": "E-Mail Adresse"
|
||||
"label": "E-Mail-Adresse"
|
||||
},
|
||||
"end-date": {
|
||||
"help-text": "Dieses Feld leer lassen, wenn noch vorhanden",
|
||||
"help-text": "Dieses Feld leer lassen, wenn dieser Eintrag noch kein Enddatum hat.",
|
||||
"label": "Enddatum"
|
||||
},
|
||||
"keywords": {
|
||||
@ -69,7 +70,7 @@
|
||||
"empty-text": "Diese Liste ist leer."
|
||||
},
|
||||
"tooltip": {
|
||||
"delete-item": "Sind Sie sicher, dass Sie dieses Element löschen möchten? Dies ist eine unumkehrbare Aktion.",
|
||||
"delete-item": "Sind Sie sicher, dass Sie dieses Element löschen möchten? Dies lässt sich nicht rückgängig machen.",
|
||||
"delete-section": "Abschnitt löschen",
|
||||
"rename-section": "Abschnitt umbenennen",
|
||||
"toggle-visibility": "Sichtbarkeit umschalten"
|
||||
@ -86,7 +87,7 @@
|
||||
"zoom-in": "Vergrößern",
|
||||
"zoom-out": "Verkleinern",
|
||||
"undo": "Rückgängig machen",
|
||||
"redo": "Redo"
|
||||
"redo": "Wiederholen"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -96,8 +97,8 @@
|
||||
"rename": "Umbenennen",
|
||||
"share-link": "Link teilen",
|
||||
"tooltips": {
|
||||
"delete": "Sind Sie sicher, dass Sie diesen Lebenslauf löschen möchten? Dies ist eine unumkehrbare Aktion.",
|
||||
"share-link": "Du musst die Sichtbarkeit deines Lebenslaufs in die Öffentlichkeit ändern, um ihn für andere sichtbar zu machen."
|
||||
"delete": "Sind Sie sicher, dass Sie diesen Lebenslauf löschen möchten? Dies lässt sich nicht rückgängig machen.",
|
||||
"share-link": "Sie müssen die Sichtbarkeit Ihres Lebenslaufs in Öffentlich ändern, um ihn für andere sichtbar zu machen."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -106,7 +107,7 @@
|
||||
"awards": {
|
||||
"form": {
|
||||
"awarder": {
|
||||
"label": "Auszeichnung"
|
||||
"label": "Auszeichner"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -119,7 +120,7 @@
|
||||
"label": "Überschrift"
|
||||
},
|
||||
"name": {
|
||||
"label": "Voller Name"
|
||||
"label": "Vollständiger Name"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Geburtsdatum"
|
||||
@ -127,7 +128,7 @@
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
"label": "Grenze"
|
||||
"label": "Rahmen"
|
||||
},
|
||||
"grayscale": {
|
||||
"label": "Graustufen"
|
||||
@ -158,13 +159,13 @@
|
||||
"education": {
|
||||
"form": {
|
||||
"area-study": {
|
||||
"label": "Studienbereich"
|
||||
"label": "Studienfach"
|
||||
},
|
||||
"courses": {
|
||||
"label": "Kurse"
|
||||
},
|
||||
"degree": {
|
||||
"label": "Grad"
|
||||
"label": "Abschluss"
|
||||
},
|
||||
"grade": {
|
||||
"label": "Note"
|
||||
@ -176,7 +177,7 @@
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "Adresse"
|
||||
"label": "Straße"
|
||||
},
|
||||
"city": {
|
||||
"label": "Stadt"
|
||||
@ -184,12 +185,12 @@
|
||||
"country": {
|
||||
"label": "Land"
|
||||
},
|
||||
"heading": "Standort",
|
||||
"heading": "Anschrift",
|
||||
"postal-code": {
|
||||
"label": "Postleitzahl"
|
||||
},
|
||||
"region": {
|
||||
"label": "Region"
|
||||
"label": "Bundesland"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
@ -201,7 +202,7 @@
|
||||
"label": "Benutzername"
|
||||
}
|
||||
},
|
||||
"heading": "Profiles",
|
||||
"heading": "Soziale Netzwerke",
|
||||
"heading_one": "Profil"
|
||||
},
|
||||
"publications": {
|
||||
@ -239,16 +240,16 @@
|
||||
"heading": "Exportieren",
|
||||
"json": {
|
||||
"primary": "JSON",
|
||||
"secondary": "Laden Sie eine JSON-Version Ihres Lebenslaufs herunter, die Sie wieder in Reaktives Lebenslauf importieren können."
|
||||
"secondary": "Laden Sie eine JSON-Version Ihres Lebenslaufs herunter, die Sie wieder in Reactive Resume importieren können."
|
||||
},
|
||||
"pdf": {
|
||||
"loading": {
|
||||
"primary": "PDF wird erstellt",
|
||||
"secondary": "Bitte warten Sie, wenn Ihr PDF generiert wird, dies kann bis zu 15 Sekunden dauern."
|
||||
"secondary": "Bitte warten Sie, während Ihr PDF generiert wird. Dies kann bis zu 15 Sekunden dauern."
|
||||
},
|
||||
"normal": {
|
||||
"primary": "PDF",
|
||||
"secondary": "Laden Sie sich ein PDF Ihres Lebenslaufs herunter, das Sie ausdrucken und an Ihren Traumjob senden können. Diese Datei kann nicht zur weiteren Bearbeitung importiert werden."
|
||||
"secondary": "Laden Sie sich ein PDF Ihres Lebenslaufs herunter, dass Sie ausdrucken oder an Ihren Traumarbeitgeber senden können. Diese Datei kann nicht zur weiteren Bearbeitung importiert werden."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -256,18 +257,20 @@
|
||||
"heading": "Layout",
|
||||
"tooltip": {
|
||||
"reset-layout": "Layout zurücksetzen"
|
||||
}
|
||||
},
|
||||
"main": "Hauptteil",
|
||||
"sidebar": "Seitenleiste"
|
||||
},
|
||||
"links": {
|
||||
"bugs-features": {
|
||||
"body": "Hält Sie etwas davon ab, einen Lebenslauf zu erstellen? Oder haben Sie eine tolle Idee, die Sie hinzufügen möchten? Erhöhen Sie einen Eintrag auf GitHub, um loszulegen.",
|
||||
"button": "GitHub Themen",
|
||||
"heading": "Fehler? Feature-Anfragen?"
|
||||
"body": "Sind Sie bei der Erstellung Ihres Lebenslaufs auf ein Problem gestoßen? Oder haben Sie eine tolle Idee, die Sie hinzufügen möchten? Erstellen Sie ein Ticket auf GitHub.",
|
||||
"button": "GitHub Issues",
|
||||
"heading": "Fehler? Verbesserungsvorschläge?"
|
||||
},
|
||||
"donate": {
|
||||
"body": "Wenn Ihnen Reactive Resume gefallen hat, denken Sie bitte darüber nach, so viel wie möglich zu spenden, damit die App für immer kostenlos und werbefrei bleibt.",
|
||||
"body": "Sollte Ihnen Reactive Resume gefallen, möchte ich Sie bitten, etwas zu spenden, damit die App für immer kostenlos und werbefrei bleibt.",
|
||||
"button": "Kaufe mir einen Kaffee",
|
||||
"heading": "Spenden an Reaktives Lebenslauf"
|
||||
"heading": "Spenden Sie an Reactive Resume."
|
||||
},
|
||||
"github": "Quellcode",
|
||||
"docs": "Dokumentation",
|
||||
@ -277,43 +280,44 @@
|
||||
"global": {
|
||||
"date": {
|
||||
"primary": "Datum",
|
||||
"secondary": "Datumsformat für die gesamte App"
|
||||
"secondary": "Datumsformat für die gesamte App.",
|
||||
"prefix": "Z.B."
|
||||
},
|
||||
"heading": "Globale",
|
||||
"heading": "Global",
|
||||
"language": {
|
||||
"primary": "Sprache",
|
||||
"secondary": "Sprache anzeigen, die in der gesamten App verwendet wird"
|
||||
"secondary": "Anzeigesprache, die in der gesamten App verwendet wird."
|
||||
},
|
||||
"theme": {
|
||||
"primary": "Thema"
|
||||
"primary": "App Design"
|
||||
}
|
||||
},
|
||||
"heading": "Einstellungen",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "Papiergröße",
|
||||
"secondary": "Legt die Abmessungen Ihrer Lebenslaufseiten fest"
|
||||
"secondary": "Legt die Seitenabmessungen Ihres Lebenslaufs fest."
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "Linie anhalten",
|
||||
"secondary": "Zeile auf allen Seiten anzeigen, um die Höhe einer A4-Seite zu markieren"
|
||||
"primary": "Seitenumbruch anzeigen",
|
||||
"secondary": "Zeigt den Seitenumbruch als Linie auf allen Seiten an."
|
||||
},
|
||||
"heading": "Seite",
|
||||
"orientation": {
|
||||
"disabled": "Hat keine Auswirkung, wenn nur eine Seite vorhanden ist",
|
||||
"disabled": "Hat keine Auswirkung, wenn nur eine Seite vorhanden ist.",
|
||||
"primary": "Ausrichtung",
|
||||
"secondary": "Ob Seiten horizontal oder vertikal angezeigt werden sollen"
|
||||
"secondary": "Legt fest, ob Seiten horizontal oder vertikal angezeigt werden sollen."
|
||||
}
|
||||
},
|
||||
"resume": {
|
||||
"heading": "Lebenslauf",
|
||||
"reset": {
|
||||
"primary": "Alles zurücksetzen",
|
||||
"secondary": "Zu viele Fehler gemacht? Klicken Sie hier, um alle Änderungen zurückzusetzen und bei Null zu beginnen. Sei vorsichtig, diese Aktion kann nicht rückgängig gemacht werden."
|
||||
"secondary": "Zu viele Fehler gemacht? Klicken Sie hier, um alle Änderungen zurückzusetzen und von vorne zu beginnen. Vorsicht! Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"sample": {
|
||||
"primary": "Beispieldaten laden",
|
||||
"secondary": "Nicht sicher, wo man anfangen soll? Klicken Sie hier, um ein paar Beispieldaten zu laden, um zu sehen, wie ein vollständiger Lebenslauf aussieht."
|
||||
"secondary": "Sie sind nicht sicher, wo Sie anfangen sollen? Klicken Sie hier, um Beispieldaten zu laden. So können Sie sich ansehen, wie ein vollständiger Lebenslauf aussieht."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -323,8 +327,8 @@
|
||||
"label": "Kurze URL bevorzugen"
|
||||
},
|
||||
"visibility": {
|
||||
"subtitle": "Erlaube jemandem mit einem Link deinen Lebenslauf anzusehen",
|
||||
"title": "Öffentlich"
|
||||
"subtitle": "Erlaubt jedem, dem Sie diesen Link schicken, Ihren Lebenslauf anzusehen.",
|
||||
"title": "Öffentlich zugänglich"
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
@ -333,16 +337,16 @@
|
||||
"theme": {
|
||||
"form": {
|
||||
"background": {
|
||||
"label": "Hintergrund"
|
||||
"label": "Hintergrundfarbe"
|
||||
},
|
||||
"primary": {
|
||||
"label": "Primär"
|
||||
"label": "Primärfarbe"
|
||||
},
|
||||
"text": {
|
||||
"label": "Text"
|
||||
"label": "Textfarbe"
|
||||
}
|
||||
},
|
||||
"heading": "Thema"
|
||||
"heading": "Lebenslauf Design"
|
||||
},
|
||||
"typography": {
|
||||
"form": {
|
||||
|
||||
@ -13,14 +13,14 @@
|
||||
"help-text": "Dieser Abschnitt unterstützt <1>Markdown</1> Formatierung."
|
||||
},
|
||||
"date": {
|
||||
"present": "Gegenwärtig"
|
||||
"present": "Heute"
|
||||
},
|
||||
"subtitle": "Ein freier und Open-Source-Lebenslauf-Builder.",
|
||||
"subtitle": "Ein kostenloser Open Source Lebenslauf-Baukasten.",
|
||||
"title": "Reaktives Lebenslauf",
|
||||
"toast": {
|
||||
"error": {
|
||||
"upload-file-size": "Bitte laden Sie nur Dateien unter 2 Megabytes hoch.",
|
||||
"upload-photo-size": "Bitte laden Sie nur Fotos unter 2 Megabytes hoch, vorzugsweise quadratisch."
|
||||
"upload-photo-size": "Bitte laden Sie nur Fotos unter 2 Megabytes hoch, am besten in einem quadratischen Format."
|
||||
},
|
||||
"success": {
|
||||
"resume-link-copied": "Ein Link zu deinem Lebenslauf wurde in deine Zwischenablage kopiert."
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
{
|
||||
"create-resume": {
|
||||
"subtitle": "Bei Null anfangen",
|
||||
"subtitle": "Mit einem leeren Lebenslauf starten",
|
||||
"title": "Neuen Lebenslauf erstellen"
|
||||
},
|
||||
"import-external": {
|
||||
"subtitle": "LinkedIn, JSON Resume, Reaktives Lebenslauf",
|
||||
"title": "Aus externen Quellen importieren"
|
||||
"subtitle": "LinkedIn, JSON Lebenslauf, Reactive Resume",
|
||||
"title": "Aus externer Quelle importieren"
|
||||
},
|
||||
"resume": {
|
||||
"menu": {
|
||||
"delete": "Löschen",
|
||||
"duplicate": "Duplikat",
|
||||
"duplicate": "Duplizieren",
|
||||
"open": "Öffnen",
|
||||
"rename": "Umbenennen",
|
||||
"share-link": "Einen Link teilen",
|
||||
"share-link": "Link teilen",
|
||||
"tooltips": {
|
||||
"delete": "Möchten Sie diesen Lebenslauf wirklich löschen? Dies ist eine irreversible Aktion.",
|
||||
"delete": "Möchten Sie diesen Lebenslauf wirklich löschen? Dies lässt sich nicht rückgängig machen.",
|
||||
"share-link": "Sie müssen die Sichtbarkeit Ihres Lebenslaufs auf öffentlich ändern, um ihn für andere sichtbar zu machen."
|
||||
}
|
||||
},
|
||||
"timestamp": "Zuletzt vor {{timestamp}} aktualisiert"
|
||||
"timestamp": "Zuletzt vor {{timestamp}} geändert."
|
||||
},
|
||||
"title": "Dashboard"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"actions": {
|
||||
"app": "Gehe zu App",
|
||||
"app": "Gehe zur App",
|
||||
"login": "Anmelden",
|
||||
"logout": "Ausloggen",
|
||||
"register": "Registrieren"
|
||||
@ -9,16 +9,16 @@
|
||||
"heading": "Eigenschaften",
|
||||
"list": {
|
||||
"ads": "Keine Werbung",
|
||||
"export": "Exportieren Sie Ihren Lebenslauf in JSON oder PDF Format",
|
||||
"free": "Frei, für immer",
|
||||
"import": "Importiere Daten von LinkedIn, JSON Lebenslauf",
|
||||
"languages": "In mehreren Sprachen zugänglich",
|
||||
"more": "Und viel mehr aufregende Features, <1>lesen Sie alles hier</1>",
|
||||
"tracking": "Keine Benutzerverfolgung"
|
||||
"export": "Exportieren Sie Ihren Lebenslauf als JSON oder PDF Format",
|
||||
"free": "Kostenlos, für immer",
|
||||
"import": "Importieren Sie Ihre Daten von LinkedIn oder als JSON Lebenslauf",
|
||||
"languages": "In mehreren Sprachen verfügbar",
|
||||
"more": "Und viele weitere aufregende Features. <1>Hier gibt es mehr informationen (in englischer Sprache)</1>",
|
||||
"tracking": "Kein Benutzertracking"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"heading": "Verknüpfungen",
|
||||
"heading": "Links",
|
||||
"links": {
|
||||
"donate": "Spenden",
|
||||
"github": "Quellcode",
|
||||
@ -32,11 +32,11 @@
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Referenzen",
|
||||
"body": "Gut oder schlecht, ich würde gerne Ihre Meinung über Reactive Resume und wie die Erfahrung war für Sie.<br/>Hier sind einige der Nachrichten, die von Benutzern auf der ganzen Welt gesendet werden.",
|
||||
"contact": "Du kannst mich über <1>meine E-Mail</1> oder über das Kontaktformular auf <3>meiner Website</3>erreichen."
|
||||
"body": "Egal ob gut oder schlecht - ich würde gerne Ihre Meinung über Reactive Resume hören und welche Erfahrungen Sie gemacht haben.<br/>Hier sind einige der Nachrichten, die mir von Benutzern auf der ganzen Welt zugesandt wurden.",
|
||||
"contact": "Sie können mich über <1>meine E-Mail</1> oder über das Kontaktformular auf <3>meiner Website</3> erreichen."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reaktives Lebenslauf ist ein freier und Open-Source-Lebenslauf-Builder, der gebaut wurde, um die weltlichen Aufgaben zu machen, zu erstellen, Aktualisieren und teilen Sie Ihren Lebenslauf so einfach wie 1, 2, 3. Mit dieser App kannst du mehrere Bewerbungen erstellen, sie mit Recruitern oder Freunden über einen einzigartigen Link teilen und sie als PDF ausdrucken. alle kostenlos, keine Werbung, keine Verfolgung, ohne die Integrität und Privatsphäre Ihrer Daten zu verlieren.",
|
||||
"body": "Reactive Resume ist ein kostenloser Open Source Lebenslauf-Builder, der gebaut wurde, um die langweilige Aufgabe einen Lebenslauf zu erstellen, aktuell zu halten und zu teilen so einfach wie möglich zu machen. Mit dieser App können Sie mehrere Lebensläufe erstellen, sie mit Recruitern oder Freunden über einen einzigartigen Link teilen und als PDF exportieren. Kostenlos, ohne Werbung, kein Tracking, ohne die Integrität und Privatsphäre Ihrer Daten zu verlieren.",
|
||||
"heading": "Zusammenfassung"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,16 +2,16 @@
|
||||
"auth": {
|
||||
"forgot-password": {
|
||||
"actions": {
|
||||
"send-email": "Passwort zurücksetzen E-Mail senden"
|
||||
"send-email": "Passwort zurücksetzen E-Mail senden."
|
||||
},
|
||||
"body": "Geben Sie einfach die E-Mail-Adresse ein, die mit dem Konto verknüpft ist, das Sie wiederherstellen möchten.",
|
||||
"body": "Geben Sie die E-Mail-Adresse des Benutzerkontos ein, dass Sie wiederherstellen möchten.",
|
||||
"form": {
|
||||
"email": {
|
||||
"label": "E-Mail-Addresse"
|
||||
}
|
||||
},
|
||||
"heading": "Passwort vergessen?",
|
||||
"help-text": "Wenn das Konto existiert, erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen Ihres Passworts."
|
||||
"help-text": "Sollte das Konto existieren, erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen des Passworts."
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
@ -24,12 +24,12 @@
|
||||
},
|
||||
"username": {
|
||||
"help-text": "Sie können auch Ihre E-Mail-Adresse eingeben",
|
||||
"label": "Nutzername"
|
||||
"label": "Benutzername"
|
||||
}
|
||||
},
|
||||
"heading": "Bei Ihrem Konto anmelden",
|
||||
"recover-text": "Falls Sie Ihr Passwort vergessen haben, können Sie <1>Ihr Konto wiederherstellen</1> hier einrichten.",
|
||||
"register-text": "Wenn Sie keinen haben, können Sie hier <1>ein Konto erstellen</1> anlegen."
|
||||
"recover-text": "Falls Sie Ihr Passwort vergessen haben, können Sie es <1>hier zurücksetzen</1>.",
|
||||
"register-text": "Sollten Sie kein Benutzerkonto haben, können Sie <1>hier ein Konto anlegen</1>."
|
||||
},
|
||||
"register": {
|
||||
"actions": {
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
"actions": {
|
||||
"add": "Add New {{token}}",
|
||||
"delete": "Delete {{token}}",
|
||||
"edit": "Edit {{token}}",
|
||||
"duplicate": "Duplicate Section"
|
||||
"duplicate": "Duplicate Section",
|
||||
"edit": "Edit {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
"heading": "Columns",
|
||||
@ -81,13 +81,13 @@
|
||||
"center-artboard": "Center Artboard",
|
||||
"copy-link": "Copy Link to Resume",
|
||||
"export-pdf": "Export PDF",
|
||||
"redo": "Redo",
|
||||
"toggle-orientation": "Toggle Page Orientation",
|
||||
"toggle-page-break-line": "Toggle Page Break Line",
|
||||
"toggle-sidebars": "Toggle Sidebars",
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -115,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Photo Filters"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Date of Birth"
|
||||
},
|
||||
"heading": "Basics",
|
||||
"headline": {
|
||||
"label": "Headline"
|
||||
@ -122,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Full Name"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Date of Birth"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -265,14 +265,15 @@
|
||||
"button": "GitHub Issues",
|
||||
"heading": "Bugs? Feature Requests?"
|
||||
},
|
||||
"docs": "Documentation",
|
||||
"donate": {
|
||||
"body": "If you liked using Reactive Resume, please consider donating as much as you can to the cause of keeping the app up and running, without ads and free forever.",
|
||||
"button": "Buy me a coffee",
|
||||
"heading": "Donate to Reactive Resume"
|
||||
},
|
||||
"github": "Source Code",
|
||||
"docs": "Documentation",
|
||||
"heading": "Links"
|
||||
"heading": "Links",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -291,14 +292,14 @@
|
||||
},
|
||||
"heading": "Settings",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "Paper Size",
|
||||
"secondary": "Determines the dimensions of your resume pages"
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "Break Line",
|
||||
"secondary": "Show a line on all pages to mark the height of an A4 page"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Paper Size",
|
||||
"secondary": "Determines the dimensions of your resume pages"
|
||||
},
|
||||
"heading": "Page",
|
||||
"orientation": {
|
||||
"disabled": "Has no effect when there is only one page",
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
"toast": {
|
||||
"error": {
|
||||
"upload-file-size": "Please upload only files under 2 megabytes.",
|
||||
"upload-photo-size": "Please upload only photos under 2 megabytes, preferrably square."
|
||||
"upload-photo-size": "Please upload only photos under 2 megabytes, preferably square."
|
||||
},
|
||||
"success": {
|
||||
"resume-link-copied": "A link to your resume has been copied to your clipboard."
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Links",
|
||||
"links": {
|
||||
"docs": "Documentation",
|
||||
"donate": "Donate",
|
||||
"github": "Source Code",
|
||||
"docs": "Documentation",
|
||||
"privacy": "Privacy Policy",
|
||||
"reddit": "Reddit",
|
||||
"service": "Terms of Service"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Screenshots"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Testimonials",
|
||||
"body": "Good or bad, I would love to hear your opinion on Reactive Resume and how the experience has been for you.<br/>Here are some of the messages sent in by users across the world.",
|
||||
"contact": "You can reach out to me through <1>my email</1> or through the contact form on <3>my website</3>."
|
||||
},
|
||||
"summary": {
|
||||
"body": "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 or friends through a unique link and print it as a PDF, all for free, no ads, no tracking, without losing the integrity and privacy of your data.",
|
||||
"heading": "Summary"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Good or bad, I would love to hear your opinion on Reactive Resume and how the experience has been for you.<br/>Here are some of the messages sent in by users across the world.",
|
||||
"contact": "You can reach out to me through <1>my email</1> or through the contact form on <3>my website</3>.",
|
||||
"heading": "Testimonials"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "Reset your password"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "Your Account",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "You can update your profile picture on <1>Gravatar</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "Full Name"
|
||||
},
|
||||
"email": {
|
||||
"label": "Email Address",
|
||||
"help-text": "It is not possible to update your email address at the moment, please create a new account instead."
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "Delete Account and Data",
|
||||
"body": "To delete your account, your data and all your resumes, type \"{{keyword}}\" into the textbox and click on the button. Please note that this is an irreversible action and your data cannot be retrieved again.",
|
||||
"actions": {
|
||||
"delete": "Delete Account"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save Changes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "Agregar nuevo {{token}}",
|
||||
"delete": "Eliminar {{token}}",
|
||||
"duplicate": "Sección duplicada",
|
||||
"edit": "Editar {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "Centrar Tablero",
|
||||
"copy-link": "Copiar enlace al currículum",
|
||||
"export-pdf": "Exportar PDF",
|
||||
"redo": "Rehacer",
|
||||
"toggle-orientation": "Cambiar la orientación de la página",
|
||||
"toggle-page-break-line": "Alternar línea de salto de página",
|
||||
"toggle-sidebars": "Ocultar/mostrar barra lateral",
|
||||
"zoom-in": "Acercar",
|
||||
"zoom-out": "Alejar",
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer"
|
||||
"zoom-in": "Acercar",
|
||||
"zoom-out": "Alejar"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Filtro de fotos"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Fecha de cumpleaños"
|
||||
},
|
||||
"heading": "Información básica",
|
||||
"headline": {
|
||||
"label": "Titular"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Nombre Completo"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Fecha de cumpleaños"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -264,14 +265,15 @@
|
||||
"button": "Propuesta de GitHub",
|
||||
"heading": "¿Errores? ¿Solicitud de características?"
|
||||
},
|
||||
"docs": "Documentación",
|
||||
"donate": {
|
||||
"body": "Si le gustó usar Reactive Resume, considere donar lo que pueda a la causa de mantener la aplicación en funcionamiento, sin anuncios y gratis para siempre.",
|
||||
"button": "Invítame a un café",
|
||||
"heading": "Donar a Reactive Resume"
|
||||
},
|
||||
"github": "Código Fuente",
|
||||
"docs": "Documentación",
|
||||
"heading": "Enlaces"
|
||||
"heading": "Enlaces",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -290,14 +292,14 @@
|
||||
},
|
||||
"heading": "Preferencias",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "Tamaño de papel",
|
||||
"secondary": "Determina las dimensiones de las páginas de tu currículum."
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "Linea de separación",
|
||||
"secondary": "Mostrar una línea en todas las páginas para marcar la altura de una página A4"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Tamaño de papel",
|
||||
"secondary": "Determina las dimensiones de las páginas de tu currículum."
|
||||
},
|
||||
"heading": "Página",
|
||||
"orientation": {
|
||||
"disabled": "No tiene efecto cuando solo hay una página",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Enlaces",
|
||||
"links": {
|
||||
"docs": "Documentación",
|
||||
"donate": "Donar",
|
||||
"github": "Código fuente",
|
||||
"docs": "Documentación",
|
||||
"privacy": "Política de Privacidad",
|
||||
"reddit": "Reddit",
|
||||
"service": "Términos de Servicio"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Capturas de pantalla"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Opiniones",
|
||||
"body": "Bueno o malo, me encantaría saber tu opinión sobre Reactive Resume y cómo ha sido la experiencia para ti.<br/>Estos son algunos de los mensajes enviados por usuarios de todo el mundo.",
|
||||
"contact": "Puedes comunicarte conmigo a través de <1>mi correo electrónico</1> o a través del formulario de contacto en <3>mi sitio web</3>."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume es un generador de currículums gratuito y de código abierto que está diseñado para hacer que las tareas mundanas de crear, actualizar y compartir su currículum sean tan fáciles como 1, 2, 3. Con esta aplicación, puede crear múltiples currículums, compartirlos con reclutadores o amigos a través de un enlace único e imprímalo como PDF, todo gratis, sin anuncios, sin seguimiento, sin perder la integridad y privacidad de sus datos.",
|
||||
"heading": "Resumen"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Bueno o malo, me encantaría saber tu opinión sobre Reactive Resume y cómo ha sido la experiencia para ti.<br/>Estos son algunos de los mensajes enviados por usuarios de todo el mundo.",
|
||||
"contact": "Puedes comunicarte conmigo a través de <1>mi correo electrónico</1> o a través del formulario de contacto en <3>mi sitio web</3>.",
|
||||
"heading": "Opiniones"
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "{{token}} جدید اضافه کنید",
|
||||
"delete": "حذف {{token}}",
|
||||
"duplicate": "بخش تکراری",
|
||||
"edit": "ویرایش {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "قرار دادن صفحه در مرکز",
|
||||
"copy-link": "کپی کردن لینک رزومه",
|
||||
"export-pdf": "خروجی PDF",
|
||||
"redo": "دوباره انجام دهید",
|
||||
"toggle-orientation": "تغییر وضعیت جهتگیری صفحه",
|
||||
"toggle-page-break-line": "تغییر وضعیت خط شکست صفحه",
|
||||
"toggle-sidebars": "باز/بسته کردن نوار کنار صفحه",
|
||||
"zoom-in": "بزرگنمایی",
|
||||
"zoom-out": "کوچکنمایی",
|
||||
"undo": "واگرد",
|
||||
"redo": "دوباره انجام دهید"
|
||||
"zoom-in": "بزرگنمایی",
|
||||
"zoom-out": "کوچکنمایی"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "فیلترهای تصویر"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "تاریخ تولد"
|
||||
},
|
||||
"heading": "موارد پایه",
|
||||
"headline": {
|
||||
"label": "سرصفحه"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "نام کامل"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "تاریخ تولد"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -264,14 +265,15 @@
|
||||
"button": "GitHub Issues",
|
||||
"heading": "باگها؟ درخواست ویژگی جدید؟"
|
||||
},
|
||||
"docs": "مستندات",
|
||||
"donate": {
|
||||
"body": "اگر استفاده از Reactive Resume را دوست داشتید، لطفاً تا جایی که می توانید کمک مالی کنید تا برنامه را بدون تبلیغات و برای همیشه رایگان نگه دارید.",
|
||||
"button": "برای من یک قهوه بخر",
|
||||
"heading": "کمک مالی به Reactive Resume"
|
||||
},
|
||||
"github": "کد منبع",
|
||||
"docs": "مستندات",
|
||||
"heading": "لینکها"
|
||||
"heading": "لینکها",
|
||||
"reddit": "ردیت"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -290,14 +292,14 @@
|
||||
},
|
||||
"heading": "تنظیمات",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "اندازه کاغذ",
|
||||
"secondary": "ابعاد صفحات رزومه شما را تعیین می کند"
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "خط شکست",
|
||||
"secondary": "برای مشخص کردن ارتفاع صفحه A4 یک خط در همه صفحات نشان داده شود"
|
||||
},
|
||||
"format": {
|
||||
"primary": "اندازه کاغذ",
|
||||
"secondary": "ابعاد صفحات رزومه شما را تعیین می کند"
|
||||
},
|
||||
"heading": "صفحه",
|
||||
"orientation": {
|
||||
"disabled": "زمانی که تنها یک صفحه وجود دارد، تاثیری ندارد",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "لینکها",
|
||||
"links": {
|
||||
"docs": "مستندات",
|
||||
"donate": "حمایت مالی",
|
||||
"github": "کد منبع",
|
||||
"docs": "مستندات",
|
||||
"privacy": "حریم خصوصی",
|
||||
"reddit": "ردیت",
|
||||
"service": "شرایط سرویسدهی"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "اسکرینشاتها"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "نظرات کاربران",
|
||||
"body": "خوب یا بد، من دوست دارم نظر شما را در مورد Reactive Resume و اینکه تجربه کار با آن برای شما چگونه بوده است را بدانم.<br/>تعدادی از پیام های ارسال شده توسط کاربران در سراسر جهان را اینجا میبینید.",
|
||||
"contact": "میتوانید از طریق <1>ایمیل من</1> یا فرم تماس در <3>وبسایت من</3> با من در ارتباط باشید."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume یک رزومه ساز رایگان و متنباز است که برای ایجاد، به روز رسانی و به اشتراک گذاری رزومه شما به آسانی شمردن ۱، ۲، ۳ ساخته شده است. با این برنامه، می توانید چندین رزومه ایجاد کنید و آنها را با کارفرماها یا دوستان از طریق یک لینک منحصر به فرد و چاپ آن به صورت PDF، همه به صورت رایگان، بدون تبلیغات، بدون ردیابی، بدون از دست دادن امنیت و حریم خصوصی داده های شما، به اشتراک بگذارید.",
|
||||
"heading": "درباره من"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "خوب یا بد، من دوست دارم نظر شما را در مورد Reactive Resume و اینکه تجربه کار با آن برای شما چگونه بوده است را بدانم.<br/>تعدادی از پیام های ارسال شده توسط کاربران در سراسر جهان را اینجا میبینید.",
|
||||
"contact": "میتوانید از طریق <1>ایمیل من</1> یا فرم تماس در <3>وبسایت من</3> با من در ارتباط باشید.",
|
||||
"heading": "نظرات کاربران"
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,7 +267,7 @@
|
||||
"donate": {
|
||||
"body": "Als Je Reactive Resume graag gebruikt, kun je overwegen zoveel mogelijk te doneren om de app in de lucht te houden, zonder advertenties en voor altijd gratis.",
|
||||
"button": "Betaal me een koffie",
|
||||
"heading": "Doneer aan Reactiv Resume"
|
||||
"heading": "Doneer aan Reactive Resume"
|
||||
},
|
||||
"github": "Broncode",
|
||||
"docs": "Documentatie",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"avatar": {
|
||||
"menu": {
|
||||
"greeting": "Hallo",
|
||||
"logout": "Afmelden"
|
||||
"logout": "Uitloggen"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
@ -20,7 +20,7 @@
|
||||
"toast": {
|
||||
"error": {
|
||||
"upload-file-size": "Upload alleen bestanden onder de 2 megabytes.",
|
||||
"upload-photo-size": "Upload alleen foto's onder de 2 megabytes, bij voorkeur vierkante."
|
||||
"upload-photo-size": "Upload alleen foto's onder de 2 megabytes, bij voorkeur vierkant."
|
||||
},
|
||||
"success": {
|
||||
"resume-link-copied": "Een link naar jouw CV is naar het klembord gekopieerd."
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /*/*
|
||||
|
||||
# Host
|
||||
Host: https://rxresu.me
|
||||
|
||||
@ -2,7 +2,7 @@ import { User } from '@reactive-resume/schema';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { setAccessToken, setUser } from '@/store/auth/authSlice';
|
||||
import { logout, setAccessToken, setUser } from '@/store/auth/authSlice';
|
||||
|
||||
import store from '../store';
|
||||
import axios from './axios';
|
||||
@ -37,6 +37,10 @@ export type ResetPasswordParams = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type UpdateProfileParams = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const login = async (loginParams: LoginParams) => {
|
||||
const {
|
||||
data: { user, accessToken },
|
||||
@ -75,3 +79,23 @@ export const resetPassword = async (resetPasswordParams: ResetPasswordParams) =>
|
||||
|
||||
toast.success('Your password has been changed successfully, please login again.');
|
||||
};
|
||||
|
||||
export const updateProfile = async (updateProfileParams: UpdateProfileParams) => {
|
||||
const { data: user } = await axios.patch<User, AxiosResponse<User>, UpdateProfileParams>(
|
||||
'/auth/update-profile',
|
||||
updateProfileParams
|
||||
);
|
||||
|
||||
store.dispatch(setUser(user));
|
||||
|
||||
toast.success('Your profile has been successfully updated.');
|
||||
};
|
||||
|
||||
export const deleteAccount = async () => {
|
||||
await axios.delete('/resume/all');
|
||||
await axios.delete('/auth');
|
||||
|
||||
store.dispatch(logout());
|
||||
|
||||
toast.success('Your account has been deleted, hope to see you again soon.');
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import _axios from 'axios';
|
||||
import _axios, { AxiosError } from 'axios';
|
||||
import Router from 'next/router';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
|
||||
@ -19,23 +20,22 @@ const axios = _axios.create({ baseURL });
|
||||
axios.interceptors.request.use((config) => {
|
||||
const { accessToken } = store.getState().auth;
|
||||
|
||||
// @ts-ignore
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
};
|
||||
config.headers.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
(error: AxiosError<ServerError>) => {
|
||||
const { response } = error;
|
||||
|
||||
if (response) {
|
||||
const errorObject: ServerError = response.data;
|
||||
const errorObject = response.data;
|
||||
const code = errorObject.statusCode;
|
||||
const message = errorObject.message;
|
||||
|
||||
toast.error(message);
|
||||
|
||||
if (code === 401 || code === 404) {
|
||||
store.dispatch(logout());
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
@ -62,9 +63,10 @@ export const fetchResumeByIdentifier = async ({
|
||||
options = { secretKey: '' },
|
||||
}: FetchResumeByIdentifierParams) => {
|
||||
if (!isBrowser) {
|
||||
const serverUrl = env('SERVER_URL');
|
||||
const secretKey = options.secretKey;
|
||||
|
||||
return axios.get<Resume>(`/resume/${username}/${slug}`, { params: { secretKey } }).then((res) => res.data);
|
||||
return fetch(`${serverUrl}/resume/${username}/${slug}?secretKey=${secretKey}`).then((response) => response.json());
|
||||
}
|
||||
|
||||
return axios.get<Resume>(`/resume/${username}/${slug}`).then((res) => res.data);
|
||||
|
||||
@ -39,7 +39,7 @@ export const buildSlice = createSlice({
|
||||
name: 'build',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTheme: (state, action: PayloadAction<SetThemePayload>) => {
|
||||
setTheme: (state: BuildState, action: PayloadAction<SetThemePayload>) => {
|
||||
const { theme } = action.payload;
|
||||
|
||||
state.theme = theme;
|
||||
|
||||
@ -5,6 +5,7 @@ export type ModalName =
|
||||
| 'auth.register'
|
||||
| 'auth.forgot'
|
||||
| 'auth.reset'
|
||||
| 'auth.profile'
|
||||
| 'dashboard.create-resume'
|
||||
| 'dashboard.import-external'
|
||||
| 'dashboard.rename-resume'
|
||||
@ -24,6 +25,7 @@ const initialState: Record<ModalName, ModalState> = {
|
||||
'auth.register': { open: false },
|
||||
'auth.forgot': { open: false },
|
||||
'auth.reset': { open: false },
|
||||
'auth.profile': { open: false },
|
||||
'dashboard.create-resume': { open: false },
|
||||
'dashboard.import-external': { open: false },
|
||||
'dashboard.rename-resume': { open: false },
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
// KaTeX (for remark-math)
|
||||
@import url('https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css');
|
||||
|
||||
// Tailwind CSS
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@ -22,7 +25,7 @@
|
||||
}
|
||||
|
||||
p {
|
||||
@apply leading-relaxed;
|
||||
@apply leading-normal;
|
||||
}
|
||||
|
||||
a {
|
||||
@ -30,7 +33,15 @@
|
||||
}
|
||||
|
||||
.markdown {
|
||||
@apply prose prose-sm leading-relaxed prose-ul:p-0 prose-ul:my-0 prose-p:my-0;
|
||||
@apply prose prose-sm leading-normal max-w-none prose-ul:p-0 prose-ul:my-0 prose-p:my-0;
|
||||
|
||||
ul li {
|
||||
@apply ml-4 list-outside;
|
||||
}
|
||||
|
||||
.footnotes p {
|
||||
@apply inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,34 +35,55 @@ export const MastheadSidebar: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className={clsx({ invert: contrast === 'light' })}>
|
||||
<h1 className="mb-1">{name}</h1>
|
||||
<p className="opacity-75">{headline}</p>
|
||||
</div>
|
||||
|
||||
<div className={clsx('flex flex-col gap-2.5', css(`svg { color: ${color} }`))}>
|
||||
<DataDisplay icon={<Room />} className="!gap-2 text-xs">
|
||||
<DataDisplay icon={<Room />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
|
||||
{formatLocation(location)}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Cake />} className="!gap-2 text-xs">
|
||||
<DataDisplay icon={<Cake />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
|
||||
{formatDateString(birthdate, dateFormat)}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Email />} className="!gap-2 text-xs" link={`mailto:${email}`}>
|
||||
<DataDisplay
|
||||
icon={<Email />}
|
||||
className="!gap-2 text-xs"
|
||||
link={`mailto:${email}`}
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{email}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Phone />} className="!gap-2 text-xs" link={`tel:${phone}`}>
|
||||
<DataDisplay
|
||||
icon={<Phone />}
|
||||
className="!gap-2 text-xs"
|
||||
link={`tel:${phone}`}
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{phone}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Public />} link={website && addHttp(website)} className="!gap-2 text-xs">
|
||||
<DataDisplay
|
||||
icon={<Public />}
|
||||
link={website && addHttp(website)}
|
||||
className="!gap-2 text-xs"
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{website}
|
||||
</DataDisplay>
|
||||
|
||||
{profiles.map(({ id, username, network, url }) => (
|
||||
<DataDisplay key={id} icon={getProfileIcon(network)} link={url && addHttp(url)} className="!gap-2 text-xs">
|
||||
<DataDisplay
|
||||
key={id}
|
||||
icon={getProfileIcon(network)}
|
||||
link={url && addHttp(url)}
|
||||
className="!gap-2 text-xs"
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{username}
|
||||
</DataDisplay>
|
||||
))}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Email, Link, Phone } from '@mui/icons-material';
|
||||
import { ListItem, Section as SectionType } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isArray from 'lodash/isArray';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -23,8 +24,10 @@ const Section: React.FC<SectionProps> = ({
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
|
||||
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
|
||||
const layout: string[][][] = useAppSelector((state) => get(state.resume.present, 'metadata.layout'));
|
||||
|
||||
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
|
||||
const isSidebarSection = useMemo(() => layout.some((row) => row[1].includes(sectionId)), [layout, sectionId]);
|
||||
|
||||
if (!section.visible) return null;
|
||||
|
||||
@ -35,7 +38,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
<Heading>{section.name}</Heading>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-4"
|
||||
className={clsx('grid items-start gap-4', { invert: isSidebarSection })}
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{section.items.map((item: ListItem) => {
|
||||
@ -76,8 +79,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
key={index}
|
||||
className="mr-2 h-3 w-3 rounded-full border"
|
||||
style={{
|
||||
borderColor: 'var(--primary-color)',
|
||||
backgroundColor: levelNum / (10 / 5) > index ? 'var(--primary-color)' : '',
|
||||
borderColor: isSidebarSection ? 'var(--text-color)' : 'var(--primary-color)',
|
||||
backgroundColor:
|
||||
levelNum / (10 / 5) > index
|
||||
? isSidebarSection
|
||||
? 'var(--text-color)'
|
||||
: 'var(--primary-color)'
|
||||
: '',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@ -94,7 +102,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
</DataDisplay>
|
||||
)}
|
||||
|
||||
{keywords && <div>{keywords.join(', ')}</div>}
|
||||
{keywords && <span>{keywords.join(', ')}</span>}
|
||||
|
||||
{(phone || email) && (
|
||||
<div className="grid gap-1">
|
||||
|
||||
@ -36,34 +36,55 @@ export const MastheadSidebar: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className={clsx({ invert: contrast === 'light' })}>
|
||||
<h1 className="mb-1">{name}</h1>
|
||||
<p className="opacity-75">{headline}</p>
|
||||
</div>
|
||||
|
||||
<div className={clsx('flex flex-col gap-2.5', css(`svg { color: ${iconColor} }`))}>
|
||||
<DataDisplay icon={<Room />} className="!gap-2 text-xs">
|
||||
<DataDisplay icon={<Room />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
|
||||
{formatLocation(location)}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Cake />} className="!gap-2 text-xs">
|
||||
<DataDisplay icon={<Cake />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
|
||||
{formatDateString(birthdate, dateFormat)}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Email />} className="!gap-2 text-xs" link={`mailto:${email}`}>
|
||||
<DataDisplay
|
||||
icon={<Email />}
|
||||
className="!gap-2 text-xs"
|
||||
link={`mailto:${email}`}
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{email}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Phone />} className="!gap-2 text-xs" link={`tel:${phone}`}>
|
||||
<DataDisplay
|
||||
icon={<Phone />}
|
||||
className="!gap-2 text-xs"
|
||||
link={`tel:${phone}`}
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{phone}
|
||||
</DataDisplay>
|
||||
|
||||
<DataDisplay icon={<Public />} link={website && addHttp(website)} className="!gap-2 text-xs">
|
||||
<DataDisplay
|
||||
icon={<Public />}
|
||||
link={website && addHttp(website)}
|
||||
className="!gap-2 text-xs"
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{website}
|
||||
</DataDisplay>
|
||||
|
||||
{profiles.map(({ id, username, network, url }) => (
|
||||
<DataDisplay key={id} icon={getProfileIcon(network)} link={url && addHttp(url)} className="!gap-2 text-xs">
|
||||
<DataDisplay
|
||||
key={id}
|
||||
icon={getProfileIcon(network)}
|
||||
link={url && addHttp(url)}
|
||||
className="!gap-2 text-xs"
|
||||
textClassName={clsx({ invert: contrast === 'light' })}
|
||||
>
|
||||
{username}
|
||||
</DataDisplay>
|
||||
))}
|
||||
|
||||
@ -10,7 +10,7 @@ import { useAppSelector } from '@/store/hooks';
|
||||
import { SectionProps } from '@/templates/sectionMap';
|
||||
import DataDisplay from '@/templates/shared/DataDisplay';
|
||||
import { formatDateString } from '@/utils/date';
|
||||
import { addHttp, parseListItemPath } from '@/utils/template';
|
||||
import { parseListItemPath } from '@/utils/template';
|
||||
|
||||
import Heading from './Heading';
|
||||
|
||||
@ -75,7 +75,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
{Array.from(Array(8).keys()).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mr-1 h-2 w-4 rounded-sm border"
|
||||
className="mr-1 h-2 w-full max-w-[1rem] rounded-sm border"
|
||||
style={{
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: levelNum / (10 / 8) > index ? primaryColor : '',
|
||||
@ -90,7 +90,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
{summary && <Markdown>{summary}</Markdown>}
|
||||
|
||||
{url && (
|
||||
<DataDisplay icon={<Link />} link={addHttp(url)}>
|
||||
<DataDisplay icon={<Link />} link={url}>
|
||||
{url}
|
||||
</DataDisplay>
|
||||
)}
|
||||
|
||||
@ -21,15 +21,8 @@ const BadgeDisplay: React.FC<Props> = ({ items }) => {
|
||||
return (
|
||||
<ul className="mt-1 flex flex-wrap gap-2 text-xs">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="rounded-sm px-2 py-0.5"
|
||||
style={{
|
||||
color: contrast === 'dark' ? theme.text : theme.background,
|
||||
backgroundColor: alpha(theme.primary, 0.75),
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
<li key={item} className="rounded-sm px-2 py-0.5" style={{ backgroundColor: alpha(theme.primary, 0.75) }}>
|
||||
<span style={{ color: contrast === 'dark' ? theme.text : theme.background }}>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -20,15 +20,8 @@ const BadgeDisplay: React.FC<Props> = ({ items }) => {
|
||||
return (
|
||||
<ul className="my-1 flex flex-wrap items-start justify-center gap-1.5">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="rounded-lg px-2 py-0.5 text-xs"
|
||||
style={{
|
||||
color: contrast === 'dark' ? theme.text : theme.background,
|
||||
backgroundColor: theme.primary,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
<li key={item} className="rounded-lg px-2 py-0.5 text-xs" style={{ backgroundColor: theme.primary }}>
|
||||
<span style={{ color: contrast === 'dark' ? theme.text : theme.background }}>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Email, Phone } from '@mui/icons-material';
|
||||
import { Email, Link, Phone } from '@mui/icons-material';
|
||||
import { ListItem, Section as SectionType } from '@reactive-resume/schema';
|
||||
import get from 'lodash/get';
|
||||
import isArray from 'lodash/isArray';
|
||||
@ -8,8 +8,9 @@ import { useMemo } from 'react';
|
||||
import Markdown from '@/components/shared/Markdown';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
import { SectionProps } from '@/templates/sectionMap';
|
||||
import DataDisplay from '@/templates/shared/DataDisplay';
|
||||
import { formatDateString } from '@/utils/date';
|
||||
import { addHttp, parseListItemPath } from '@/utils/template';
|
||||
import { parseListItemPath } from '@/utils/template';
|
||||
|
||||
import BadgeDisplay from './BadgeDisplay';
|
||||
import Heading from './Heading';
|
||||
@ -87,9 +88,9 @@ const Section: React.FC<SectionProps> = ({
|
||||
|
||||
{url && (
|
||||
<div className="inline-flex justify-center">
|
||||
<a href={addHttp(url)} target="_blank" rel="noreferrer">
|
||||
<DataDisplay link={url} icon={<Link />}>
|
||||
{url}
|
||||
</a>
|
||||
</DataDisplay>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.container {
|
||||
@apply grid grid-cols-2 gap-4 px-6 py-4;
|
||||
@apply grid grid-cols-2 gap-4 px-6 py-4 items-start;
|
||||
|
||||
.main {
|
||||
@apply grid gap-4;
|
||||
|
||||
@ -10,7 +10,7 @@ import { useAppSelector } from '@/store/hooks';
|
||||
import { SectionProps } from '@/templates/sectionMap';
|
||||
import DataDisplay from '@/templates/shared/DataDisplay';
|
||||
import { formatDateString } from '@/utils/date';
|
||||
import { addHttp, parseListItemPath } from '@/utils/template';
|
||||
import { parseListItemPath } from '@/utils/template';
|
||||
|
||||
import Heading from './Heading';
|
||||
|
||||
@ -90,7 +90,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
{summary && <Markdown>{summary}</Markdown>}
|
||||
|
||||
{url && (
|
||||
<DataDisplay icon={<Link />} link={addHttp(url)} className="text-xs">
|
||||
<DataDisplay icon={<Link />} link={url} className="text-xs">
|
||||
{url}
|
||||
</DataDisplay>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
|
||||
import { ThemeConfig } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useMemo } from 'react';
|
||||
@ -72,14 +73,14 @@ export const MastheadMain: React.FC = () => {
|
||||
className="grid gap-2 p-4"
|
||||
style={{ color: contrast === 'dark' ? theme.text : theme.background, backgroundColor: theme.primary }}
|
||||
>
|
||||
<div>
|
||||
<div className={clsx({ invert: contrast === 'light' })}>
|
||||
<h1>{name}</h1>
|
||||
<p className="opacity-75">{headline}</p>
|
||||
</div>
|
||||
|
||||
<hr className="opacity-25" />
|
||||
|
||||
<Markdown>{summary}</Markdown>
|
||||
<Markdown className={clsx({ invert: contrast === 'light' })}>{summary}</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import clsx from 'clsx';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import { addHttp } from '@/utils/template';
|
||||
|
||||
type Props = {
|
||||
icon?: JSX.Element;
|
||||
link?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
};
|
||||
|
||||
const DataDisplay: React.FC<React.PropsWithChildren<Props>> = ({ icon, link, className, children }) => {
|
||||
const DataDisplay: React.FC<React.PropsWithChildren<Props>> = ({ icon, link, className, textClassName, children }) => {
|
||||
if (isEmpty(children)) return null;
|
||||
|
||||
if (!isEmpty(link)) {
|
||||
if (link && !isEmpty(link)) {
|
||||
return (
|
||||
<div className={clsx('inline-flex items-center gap-1', className)}>
|
||||
{icon}
|
||||
<a href={link} target="_blank" rel="noreferrer">
|
||||
<a target="_blank" rel="noreferrer" href={addHttp(link)} className={textClassName}>
|
||||
{children}
|
||||
</a>
|
||||
</div>
|
||||
@ -24,7 +27,7 @@ const DataDisplay: React.FC<React.PropsWithChildren<Props>> = ({ icon, link, cla
|
||||
return (
|
||||
<div className={clsx('inline-flex items-center gap-1', className)}>
|
||||
{icon}
|
||||
<span>{children}</span>
|
||||
<span className={textClassName}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ export const dateFormatOptions: string[] = [
|
||||
'DD.MM.YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'MM.DD.YYYY',
|
||||
'M.D.YYYY',
|
||||
'MM/DD/YYYY',
|
||||
'YYYY.MM.DD',
|
||||
'YYYY/MM/DD',
|
||||
@ -19,7 +20,7 @@ export const dateFormatOptions: string[] = [
|
||||
'YYYY',
|
||||
];
|
||||
|
||||
export const getRelativeTime = (timestamp: dayjs.ConfigType): string => dayjs(timestamp).utc().toNow(true);
|
||||
export const getRelativeTime = (timestamp: dayjs.ConfigType): string => dayjs(timestamp).toNow(true);
|
||||
|
||||
export const formatDateString = (date: string | DateRange, formatStr: string): string | null => {
|
||||
const presentString = i18n?.t<string>('common.date.present') ?? '';
|
||||
@ -30,7 +31,7 @@ export const formatDateString = (date: string | DateRange, formatStr: string): s
|
||||
if (isString(date)) {
|
||||
if (!dayjs(date).isValid()) return null;
|
||||
|
||||
return dayjs(date).utc(true).format(formatStr);
|
||||
return dayjs.utc(date).local().format(formatStr);
|
||||
}
|
||||
|
||||
// If `date` is a DateRange
|
||||
@ -38,9 +39,13 @@ export const formatDateString = (date: string | DateRange, formatStr: string): s
|
||||
|
||||
if (!dayjs(date.start).isValid()) return null;
|
||||
|
||||
if (!isEmpty(date.end) && dayjs(date.end).isValid()) {
|
||||
return `${dayjs(date.start).utc(true).format(formatStr)} - ${dayjs(date.end).utc(true).format(formatStr)}`;
|
||||
if (dayjs(date.start).isSame(date.end)) {
|
||||
return dayjs.utc(date.start).local().format(formatStr);
|
||||
}
|
||||
|
||||
return `${dayjs(date.start).utc(true).format(formatStr)} - ${presentString}`;
|
||||
if (!isEmpty(date.end) && dayjs(date.end).isValid()) {
|
||||
return `${dayjs.utc(date.start).local().format(formatStr)} - ${dayjs.utc(date.end).local().format(formatStr)}`;
|
||||
}
|
||||
|
||||
return `${dayjs.utc(date.start).local().format(formatStr)} - ${presentString}`;
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
FaHackerrank,
|
||||
FaInstagram,
|
||||
FaLinkedinIn,
|
||||
FaMastodon,
|
||||
FaMedium,
|
||||
FaSkype,
|
||||
FaSoundcloud,
|
||||
@ -18,10 +19,11 @@ import {
|
||||
FaXing,
|
||||
FaYoutube,
|
||||
} from 'react-icons/fa';
|
||||
import { SiCodechef, SiCodeforces } from 'react-icons/si';
|
||||
import { SiCodeberg, SiCodechef, SiCodeforces } from 'react-icons/si';
|
||||
|
||||
const profileIconMap: Record<string, JSX.Element> = {
|
||||
behance: <FaBehance />,
|
||||
codeberg: <SiCodeberg />,
|
||||
codechef: <SiCodechef />,
|
||||
codeforces: <SiCodeforces />,
|
||||
dribbble: <FaDribbble />,
|
||||
@ -31,6 +33,7 @@ const profileIconMap: Record<string, JSX.Element> = {
|
||||
hackerrank: <FaHackerrank />,
|
||||
instagram: <FaInstagram />,
|
||||
linkedin: <FaLinkedinIn />,
|
||||
mastodon: <FaMastodon />,
|
||||
medium: <FaMedium />,
|
||||
skype: <FaSkype />,
|
||||
soundcloud: <FaSoundcloud />,
|
||||
|
||||
@ -4,10 +4,10 @@ import { RgbColor } from 'react-colorful';
|
||||
import { hexColorPattern } from '@/config/colors';
|
||||
|
||||
export const generateTypographyStyles = ({ family, size }: Typography): string => `
|
||||
font-size: ${size.body}px;
|
||||
font-family: ${family.body};
|
||||
font-size: ${size.body}px !important;
|
||||
font-family: ${family.body} !important;
|
||||
|
||||
svg { font-size: ${size.body}px; }
|
||||
p, li, svg { font-size: ${size.body}px !important; line-height: ${size.body * 1.5}px !important; }
|
||||
|
||||
h1,
|
||||
h2,
|
||||
@ -15,22 +15,38 @@ export const generateTypographyStyles = ({ family, size }: Typography): string =
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
font-family: ${family.heading};
|
||||
font-weight: bold !important;
|
||||
font-family: ${family.heading} !important;
|
||||
}
|
||||
|
||||
h1 { font-size: ${size.heading}px; line-height: ${size.heading}px; }
|
||||
h2 { font-size: ${size.heading / 1.5}px; line-height: ${size.heading / 1.5}px; }
|
||||
h3 { font-size: ${size.heading / 2}px; line-height: ${size.heading / 2}px; }
|
||||
h4 { font-size: ${size.heading / 2.5}px; line-height: ${size.heading / 2.5}px; }
|
||||
h5 { font-size: ${size.heading / 3}px; line-height: ${size.heading / 3}px; }
|
||||
h6 { font-size: ${size.heading / 3.5}px; line-height: ${size.heading / 3.5}px; }
|
||||
h1 { font-size: ${size.heading}px !important; line-height: ${size.heading}px !important; }
|
||||
h2 { font-size: ${size.heading / 1.5}px !important; line-height: ${size.heading / 1.5}px !important; }
|
||||
h3 { font-size: ${size.heading / 2}px !important; line-height: ${size.heading / 2}px !important; }
|
||||
h4 { font-size: ${size.heading / 2.5}px !important; line-height: ${size.heading / 2.5}px !important; }
|
||||
h5 { font-size: ${size.heading / 3}px !important; line-height: ${size.heading / 3}px !important; }
|
||||
h6 { font-size: ${size.heading / 3.5}px !important; line-height: ${size.heading / 3.5}px !important; }
|
||||
`;
|
||||
|
||||
export const generateThemeStyles = ({ text, background, primary }: ThemeConfig): string => `
|
||||
color: ${text};
|
||||
background-color: ${background};
|
||||
--primary-color: ${primary};
|
||||
--text-color: ${text} !important;
|
||||
--primary-color: ${primary} !important;
|
||||
--background-color: ${background} !important;
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
|
||||
span,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
li,
|
||||
p,
|
||||
a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--primary-color);
|
||||
|
||||
@ -20,7 +20,7 @@ export const formatLocation = (location?: Location): string => {
|
||||
};
|
||||
|
||||
export const addHttp = (url: string) => {
|
||||
if (url.search(/^http[s]?:\/\//) == -1) {
|
||||
if (url.search(/^http[s]?:\/\//) == -1 && url.search(/^mailto:/) == -1 && url.search(/^tel:/) == -1) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
|
||||
@ -13,9 +13,6 @@ const DateWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) =
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
// Set Default Timezone to UTC
|
||||
dayjs.tz.setDefault('UTC');
|
||||
|
||||
// Locales
|
||||
require('dayjs/locale/am');
|
||||
require('dayjs/locale/ar');
|
||||
|
||||
@ -2,8 +2,7 @@ import DateWrapper from './DateWrapper';
|
||||
import FontWrapper from './FontWrapper';
|
||||
import ThemeWrapper from './ThemeWrapper';
|
||||
|
||||
const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return (
|
||||
const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
|
||||
<ThemeWrapper>
|
||||
<FontWrapper>
|
||||
<DateWrapper>
|
||||
@ -12,6 +11,5 @@ const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children
|
||||
</FontWrapper>
|
||||
</ThemeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrapperRegistry;
|
||||
|
||||
36
package.json
36
package.json
@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "reactive-resume",
|
||||
"version": "3.6.9",
|
||||
"version": "3.6.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"build": "env-cmd --silent turbo run build",
|
||||
"start": "env-cmd --silent turbo run start",
|
||||
"dev": "env-cmd --silent cross-var cross-env VERSION=$npm_package_version turbo run dev",
|
||||
"build": "env-cmd --silent cross-var cross-env VERSION=$npm_package_version turbo run build",
|
||||
"start": "env-cmd --silent cross-var cross-env VERSION=$npm_package_version turbo run start",
|
||||
"update-deps": "ncu -x nanoid,class-validator --deep -u && pnpm install",
|
||||
"generate-env": "ts-node ./scripts/generate-env.ts",
|
||||
"format": "prettier --write .",
|
||||
"release": "standard-version --release-as patch"
|
||||
"lint": "turbo run lint"
|
||||
},
|
||||
"workspaces": [
|
||||
"schema",
|
||||
@ -16,18 +17,23 @@
|
||||
"server"
|
||||
],
|
||||
"dependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-var": "^1.1.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"turbo": "^1.6.3"
|
||||
"turbo": "^1.7.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "^4.8.4"
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-simple-import-sort": "^9.0.0",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"prettier": "^2.8.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.2",
|
||||
|
||||
6037
pnpm-lock.yaml
generated
6037
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.27.0",
|
||||
"typescript": "^4.8.4"
|
||||
"eslint": "^8.32.0",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
66
scripts/generate-env.ts
Normal file
66
scripts/generate-env.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
const envMap = {
|
||||
TZ: 'UTC',
|
||||
PUBLIC_URL: '',
|
||||
PUBLIC_SERVER_URL: '',
|
||||
PUBLIC_GOOGLE_CLIENT_ID: '',
|
||||
POSTGRES_DB: 'postgres',
|
||||
POSTGRES_USER: 'postgres',
|
||||
POSTGRES_PASSWORD: 'postgres',
|
||||
SECRET_KEY: '',
|
||||
POSTGRES_HOST: 'localhost',
|
||||
POSTGRES_PORT: '5432',
|
||||
POSTGRES_SSL_CERT: '',
|
||||
JWT_SECRET: '',
|
||||
JWT_EXPIRY_TIME: '604800',
|
||||
GOOGLE_CLIENT_SECRET: '',
|
||||
GOOGLE_API_KEY: '',
|
||||
MAIL_FROM_NAME: '',
|
||||
MAIL_FROM_EMAIL: '',
|
||||
MAIL_HOST: '',
|
||||
MAIL_PORT: '',
|
||||
MAIL_USERNAME: '',
|
||||
MAIL_PASSWORD: '',
|
||||
STORAGE_BUCKET: '',
|
||||
STORAGE_REGION: '',
|
||||
STORAGE_ENDPOINT: '',
|
||||
STORAGE_URL_PREFIX: '',
|
||||
STORAGE_ACCESS_KEY: '',
|
||||
STORAGE_SECRET_KEY: '',
|
||||
PDF_DELETION_TIME: '345600000',
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS: 'false',
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
// URLs
|
||||
// If running in a Gitpod environment, auto generated the URLs
|
||||
if (process.env.GITPOD_WORKSPACE_URL) {
|
||||
const baseUrl = new URL(process.env.GITPOD_WORKSPACE_URL!).host;
|
||||
|
||||
envMap['PUBLIC_SERVER_URL'] = `https://3100-${baseUrl}`;
|
||||
envMap['PUBLIC_URL'] = `https://3000-${baseUrl}`;
|
||||
}
|
||||
// Otherwise, fallback to localhost
|
||||
else {
|
||||
envMap['PUBLIC_SERVER_URL'] = 'https://localhost:3100';
|
||||
envMap['PUBLIC_URL'] = 'https://localhost:3000';
|
||||
}
|
||||
|
||||
// Secret Key
|
||||
envMap['SECRET_KEY'] = randomBytes(20).toString('hex');
|
||||
envMap['JWT_SECRET'] = randomBytes(40).toString('hex');
|
||||
|
||||
const envFile = Object.entries(envMap)
|
||||
.reduce((acc, [key, value]) => {
|
||||
acc.push(`${key}=${value}`);
|
||||
|
||||
return acc;
|
||||
}, [] as string[])
|
||||
.join('\n');
|
||||
|
||||
await fs.writeFile('.env', envFile);
|
||||
};
|
||||
|
||||
main();
|
||||
@ -21,15 +21,9 @@ COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
|
||||
COPY --from=dependencies /app/server/node_modules ./server/node_modules
|
||||
|
||||
ARG TURBO_TEAM
|
||||
ARG TURBO_TOKEN
|
||||
|
||||
ENV TURBO_TEAM $TURBO_TEAM
|
||||
ENV TURBO_TOKEN $TURBO_TOKEN
|
||||
|
||||
RUN pnpm run build --filter server
|
||||
|
||||
FROM mcr.microsoft.com/playwright:next-jammy as production
|
||||
FROM mcr.microsoft.com/playwright:v1.29.2-focal as production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@ -8,65 +8,66 @@
|
||||
"start": "node dist/main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.209.0",
|
||||
"@nestjs/axios": "^1.0.0",
|
||||
"@nestjs/common": "^9.2.0",
|
||||
"@aws-sdk/client-s3": "^3.252.0",
|
||||
"@nestjs/axios": "^1.0.1",
|
||||
"@nestjs/common": "^9.2.1",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.2.0",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/core": "^9.2.1",
|
||||
"@nestjs/jwt": "^10.0.1",
|
||||
"@nestjs/mapped-types": "^1.2.0",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.2.0",
|
||||
"@nestjs/platform-express": "^9.2.1",
|
||||
"@nestjs/schedule": "^2.1.0",
|
||||
"@nestjs/serve-static": "^3.0.0",
|
||||
"@nestjs/terminus": "^9.1.2",
|
||||
"@nestjs/terminus": "^9.1.4",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@types/passport": "^1.0.11",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager": "^5.1.3",
|
||||
"cache-manager": "^5.1.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"class-validator": "^0.13.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csvtojson": "^2.0.10",
|
||||
"dayjs": "^1.11.6",
|
||||
"dayjs": "^1.11.7",
|
||||
"google-auth-library": "^8.7.0",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"nanoid": "^3.3.4",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nanoid": "3.3.4",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"nodemailer": "^6.8.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.8.0",
|
||||
"playwright-chromium": "^1.27.1",
|
||||
"playwright-chromium": "^1.29.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.7",
|
||||
"typeorm": "0.3.10",
|
||||
"rimraf": "^4.1.1",
|
||||
"rxjs": "^7.8.0",
|
||||
"typeorm": "0.3.11",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@nestjs/cli": "^9.1.8",
|
||||
"@nestjs/schematics": "^9.0.4",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/passport-jwt": "^3.0.7",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"prettier": "^2.7.1",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"prettier": "^2.8.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.8.4",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Post, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { User } from '@/decorators/user.decorator';
|
||||
import { User as UserEntity } from '@/users/entities/user.entity';
|
||||
@ -7,6 +7,7 @@ import { AuthService } from './auth.service';
|
||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||
import { LocalAuthGuard } from './guards/local.guard';
|
||||
|
||||
@ -57,6 +58,13 @@ export class AuthController {
|
||||
return this.authService.resetPassword(resetPasswordDto);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Patch('update-profile')
|
||||
updateProfile(@User('id') userId: number, @Body() updateProfileDto: UpdateProfileDto) {
|
||||
return this.authService.updateProfile(userId, updateProfileDto);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete()
|
||||
|
||||
@ -6,12 +6,14 @@ import { compareSync, hashSync } from 'bcryptjs';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
|
||||
import { PostgresErrorCode } from '@/database/errorCodes.enum';
|
||||
import { ResumeService } from '@/resume/resume.service';
|
||||
import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto';
|
||||
import { User } from '@/users/entities/user.entity';
|
||||
import { UsersService } from '@/users/users.service';
|
||||
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -68,6 +70,10 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
updateProfile(id: number, newData: UpdateProfileDto) {
|
||||
return this.usersService.update(id, { name: newData.name });
|
||||
}
|
||||
|
||||
forgotPassword(email: string) {
|
||||
return this.usersService.generateResetToken(email);
|
||||
}
|
||||
|
||||
7
server/src/auth/dto/update-profile.dto.ts
Normal file
7
server/src/auth/dto/update-profile.dto.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('app', () => ({
|
||||
timezone: process.env.TZ,
|
||||
version: process.env.VERSION,
|
||||
environment: process.env.NODE_ENV,
|
||||
secretKey: process.env.SECRET_KEY,
|
||||
port: parseInt(process.env.PORT, 10) || 3100,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('cache', () => ({
|
||||
pdfDeletionTime: parseInt(process.env.PDF_DELETION_TIME, 10),
|
||||
pdfDeletionTime: parseInt(process.env.PDF_DELETION_TIME, 10) || 4 * 24 * 60 * 60 * 1000, // 4 days
|
||||
}));
|
||||
|
||||
@ -14,6 +14,7 @@ const validationSchema = Joi.object({
|
||||
// App
|
||||
TZ: Joi.string().default('UTC'),
|
||||
PORT: Joi.number().default(3100),
|
||||
VERSION: Joi.string().required(),
|
||||
SECRET_KEY: Joi.string().required(),
|
||||
NODE_ENV: Joi.string().valid('development', 'production').default('development'),
|
||||
|
||||
@ -55,7 +56,9 @@ const validationSchema = Joi.object({
|
||||
STORAGE_SECRET_KEY: Joi.string().allow(''),
|
||||
|
||||
// Cache
|
||||
PDF_DELETION_TIME: Joi.number().default(4 * 24 * 60 * 60 * 1000), // 4 days
|
||||
PDF_DELETION_TIME: Joi.number()
|
||||
.default(4 * 24 * 60 * 60 * 1000) // 4 days
|
||||
.allow(''),
|
||||
});
|
||||
|
||||
@Module({
|
||||
|
||||
@ -15,7 +15,6 @@ import {
|
||||
} from '@reactive-resume/schema';
|
||||
import csv from 'csvtojson';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { readFile, unlink } from 'fs/promises';
|
||||
import { cloneDeep, get, isEmpty, merge } from 'lodash';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
@ -29,9 +28,7 @@ import { ResumeService } from '@/resume/resume.service';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationsService {
|
||||
constructor(private resumeService: ResumeService) {
|
||||
dayjs.extend(utc);
|
||||
}
|
||||
constructor(private resumeService: ResumeService) {}
|
||||
|
||||
async linkedIn(userId: number, path: string): Promise<ResumeEntity> {
|
||||
let archive: StreamZip.StreamZipAsync;
|
||||
@ -43,7 +40,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(defaultState);
|
||||
|
||||
// Basics
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from LinkedIn (${timestamp})`,
|
||||
slug: `imported-from-linkedin-${timestamp}`,
|
||||
@ -276,7 +273,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(defaultState);
|
||||
|
||||
// Metadata
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from JSON Resume (${timestamp})`,
|
||||
slug: `imported-from-json-resume-${timestamp}`,
|
||||
@ -611,7 +608,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(jsonResume);
|
||||
|
||||
// Metadata
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from Reactive Resume (${timestamp})`,
|
||||
slug: `imported-from-reactive-resume-${timestamp}`,
|
||||
@ -632,7 +629,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(defaultState);
|
||||
|
||||
// Metadata
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from Reactive Resume V2 (${timestamp})`,
|
||||
slug: `imported-from-reactive-resume-v2-${timestamp}`,
|
||||
@ -958,6 +955,6 @@ export class IntegrationsService {
|
||||
}
|
||||
|
||||
private parseDate = (date: string): string => {
|
||||
return isEmpty(date) ? '' : dayjs(date).utc().toISOString();
|
||||
return isEmpty(date) ? '' : dayjs(date).toISOString();
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,18 +8,20 @@ import { AppModule } from './app.module';
|
||||
|
||||
const bootstrap = async () => {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
const appUrl = configService.get<string>('app.url');
|
||||
|
||||
// Middleware
|
||||
app.enableCors({ credentials: true });
|
||||
app.enableCors({ origin: [appUrl], credentials: true });
|
||||
app.enableShutdownHooks();
|
||||
app.use(cookieParser());
|
||||
|
||||
// Pipes
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
// Server Port
|
||||
const port = configService.get<number>('app.port');
|
||||
|
||||
await app.listen(port);
|
||||
|
||||
Logger.log(`🚀 Server is up and running!`);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { Controller, Get, InternalServerErrorException, Param, Query } from '@nestjs/common';
|
||||
|
||||
import { PrinterService } from './printer.service';
|
||||
|
||||
|
||||
@ -31,10 +31,8 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
const publicUrl = `${serverUrl}/assets/exports/${filename}`;
|
||||
|
||||
try {
|
||||
// check if file already exists
|
||||
await access(join(directory, filename));
|
||||
} catch {
|
||||
// delete old files and scheduler jobs
|
||||
const activeSchedulerTimeouts = this.schedulerRegistry.getTimeouts();
|
||||
await readdir(directory).then(async (files) => {
|
||||
await Promise.all(
|
||||
@ -49,7 +47,6 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
});
|
||||
|
||||
// create file as it doesn't exist
|
||||
const url = this.configService.get<string>('app.url');
|
||||
const secretKey = this.configService.get<string>('app.secretKey');
|
||||
const pdfDeletionTime = this.configService.get<number>('cache.pdfDeletionTime');
|
||||
@ -57,6 +54,7 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
const page = await this.browser.newPage();
|
||||
|
||||
await page.goto(`${url}/${username}/${slug}/printer?secretKey=${secretKey}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('html.wf-active');
|
||||
|
||||
const pageFormat: PageConfig['format'] = await page.$$eval(
|
||||
|
||||
@ -5,9 +5,9 @@ const sampleData: Partial<Resume> = {
|
||||
name: 'Alexis Jones',
|
||||
email: 'alexis.jones@gmail.com',
|
||||
phone: '+1 800 1200 3820',
|
||||
birthdate: '1995-08-06T00:00:00.000Z',
|
||||
birthdate: '1995-08-06',
|
||||
photo: {
|
||||
url: `/images/sample-photo.jpg`,
|
||||
url: `https://i.imgur.com/O7iT9ke.jpg`,
|
||||
filters: {
|
||||
size: 128,
|
||||
shape: 'rounded-square',
|
||||
@ -53,7 +53,7 @@ const sampleData: Partial<Resume> = {
|
||||
url: 'https://www.espritcam.com',
|
||||
date: {
|
||||
end: '',
|
||||
start: '2015-09-01T16:34:27.000Z',
|
||||
start: '2015-09-01',
|
||||
},
|
||||
name: 'DP Technology Corp.',
|
||||
summary:
|
||||
@ -64,8 +64,8 @@ const sampleData: Partial<Resume> = {
|
||||
id: '285d78f8-df56-4569-ba6b-cff5ebe5381e',
|
||||
url: 'https://www.vokophone.com',
|
||||
date: {
|
||||
end: '2015-07-31T22:00:00.000Z',
|
||||
start: '2011-05-31T22:00:00.000Z',
|
||||
end: '2015-07-31',
|
||||
start: '2011-05-31',
|
||||
},
|
||||
name: 'Voko Communications',
|
||||
summary:
|
||||
@ -84,7 +84,7 @@ const sampleData: Partial<Resume> = {
|
||||
{
|
||||
title: 'Blitz Hackathon',
|
||||
awarder: '2nd Place',
|
||||
date: '2018-03-31T22:00:00.000Z',
|
||||
date: '2018-03-31',
|
||||
url: '',
|
||||
summary: '',
|
||||
id: '657cadb0-c07d-4a35-8351-9079598c7ac0',
|
||||
@ -92,7 +92,7 @@ const sampleData: Partial<Resume> = {
|
||||
{
|
||||
title: 'Carl-Zeiss Hackathon',
|
||||
awarder: '2nd Place',
|
||||
date: '2017-05-09T22:00:00.000Z',
|
||||
date: '2017-05-09',
|
||||
url: '',
|
||||
summary: '',
|
||||
id: 'db3bc5cb-483e-4221-9867-9c28ee5f2051',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user