mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d01d6a833 | |||
| 1914ebb9ae | |||
| 686dba90c9 | |||
| 95dc3bf571 | |||
| 1c8fdbf848 | |||
| d8357c9959 | |||
| 90e994377b | |||
| 82c6ee6d5d | |||
| 7b615e73c3 | |||
| 268e4a87fe | |||
| 73f8eb84c9 | |||
| a31ef89996 | |||
| d6bca7ebab | |||
| e0a42fd928 | |||
| deb4e0a0de | |||
| a687062866 | |||
| 700439c8a8 | |||
| 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 | |||
| 8026241b6c | |||
| 89b35392bd | |||
| 62eb239ec4 | |||
| 7fdf8c1f0c | |||
| 538697238a | |||
| 7bc4a998fe | |||
| e33df485ab | |||
| 36ae54fe17 | |||
| 50958fd6df | |||
| e9e595f0d0 | |||
| 43ddfba777 | |||
| 78a32961d7 | |||
| 9b1f3eda05 | |||
| 1154621e5c | |||
| e7aeee77a7 | |||
| fab3988a36 | |||
| 354cad88d3 | |||
| 876f930f30 | |||
| 5b3ea46f0f | |||
| 37a2563c11 | |||
| cb977a146b | |||
| 72b2551b6d | |||
| c94633e616 | |||
| 7fee2d670f | |||
| 837b06eb38 | |||
| 2b8860b21c | |||
| 3a7b98d30e | |||
| fc0b69796f |
@ -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
|
||||
|
||||
@ -34,6 +34,7 @@ STORAGE_ENDPOINT=
|
||||
STORAGE_URL_PREFIX=
|
||||
STORAGE_ACCESS_KEY=
|
||||
STORAGE_SECRET_KEY=
|
||||
PDF_DELETION_TIME=345600000
|
||||
|
||||
# Flags (Client)
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
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.1.1
|
||||
uses: digitalocean/action-doctl@v2.2.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
|
||||
|
||||
96
.github/workflows/docker-build-push.yml
vendored
96
.github/workflows/docker-build-push.yml
vendored
@ -1,112 +1,62 @@
|
||||
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
|
||||
|
||||
- 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.0.0
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: amd64
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.0.0
|
||||
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.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Client Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v3.2.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.0.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.0.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.1.1
|
||||
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 }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,4 +10,7 @@ node_modules
|
||||
.DS_Store
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
.turbo
|
||||
|
||||
# Intellij
|
||||
.idea
|
||||
|
||||
@ -18,7 +18,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](https://docs.rxresu.me)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Languages](#languages)
|
||||
@ -93,7 +93,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 +104,7 @@ 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.
|
||||
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
|
||||
|
||||
|
||||
@ -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.7.21' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
2
client/.gitignore
vendored
2
client/.gitignore
vendored
@ -39,4 +39,4 @@ yarn-error.log*
|
||||
__ENV.js
|
||||
|
||||
# next-sitemap
|
||||
sitemap*.xml
|
||||
sitemap*.xml
|
||||
|
||||
@ -46,6 +46,6 @@ EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||
CMD curl -fSs 127.0.0.1:3000 || exit 1
|
||||
CMD curl -fSs localhost:3000 || exit 1
|
||||
|
||||
CMD [ "pnpm", "run", "start", "--filter", "client" ]
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { ButtonBase, Divider, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { get } from 'lodash';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -67,8 +68,9 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
|
||||
const slug = get(resume, 'slug');
|
||||
const username = get(resume, 'user.username');
|
||||
const updatedAt = get(resume, 'updatedAt');
|
||||
|
||||
const url = await mutateAsync({ username, slug });
|
||||
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
|
||||
|
||||
download(url);
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@apply h-full w-full #{!important};
|
||||
@apply h-full w-full overflow-visible #{!important};
|
||||
}
|
||||
|
||||
.artboard {
|
||||
|
||||
@ -36,13 +36,6 @@
|
||||
top: calc(279mm - 19px);
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
text-indent: -1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pageNumber {
|
||||
|
||||
@ -26,24 +26,24 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
const theme: ThemeConfig = get(resume, 'metadata.theme');
|
||||
const customCSS: CustomCSS = get(resume, 'metadata.css');
|
||||
const template: string = get(resume, 'metadata.template');
|
||||
const pageConfig: PageConfig = get(resume, 'metadata.page');
|
||||
const typography: Typography = get(resume, 'metadata.typography');
|
||||
const pageConfig: PageConfig = get(resume, 'metadata.page', {} as PageConfig);
|
||||
|
||||
const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
|
||||
const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);
|
||||
const TemplatePage: React.FC<PageProps> | null = useMemo(() => templateMap[template].component, [template]);
|
||||
|
||||
return (
|
||||
<div data-page={page + 1} data-format={pageConfig.format || 'A4'} className={styles.container}>
|
||||
<div className={styles.container} data-page={page + 1} data-format={pageConfig?.format || 'A4'}>
|
||||
<div
|
||||
className={clsx({
|
||||
reset: true,
|
||||
[styles.page]: true,
|
||||
[styles.break]: breakLine,
|
||||
[styles['format-letter']]: pageConfig?.format === 'Letter',
|
||||
[css(themeCSS)]: true,
|
||||
[css(typographyCSS)]: true,
|
||||
[css(customCSS.value)]: customCSS.visible,
|
||||
[styles['format-letter']]: pageConfig?.format === 'Letter',
|
||||
})}
|
||||
>
|
||||
{TemplatePage && <TemplatePage page={page} />}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { Add, Star } from '@mui/icons-material';
|
||||
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Section as SectionRecord } from '@reactive-resume/schema';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import React, { ReactComponentElement, useMemo } from 'react';
|
||||
import { validate } from 'uuid';
|
||||
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import { getCustomSections, left } from '@/config/sections';
|
||||
import { getCustomSections, getSectionsByType, left } from '@/config/sections';
|
||||
import { setSidebarState } from '@/store/build/buildSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { addSection } from '@/store/resume/resumeSlice';
|
||||
@ -52,7 +53,49 @@ const LeftSidebar = () => {
|
||||
items: [],
|
||||
};
|
||||
|
||||
dispatch(addSection({ value: newSection }));
|
||||
dispatch(addSection({ value: newSection, type: 'custom' }));
|
||||
};
|
||||
|
||||
const sectionsList = () => {
|
||||
const sectionsComponents: Array<ReactComponentElement<any>> = [];
|
||||
|
||||
for (const item of left) {
|
||||
const id = (item as any).id;
|
||||
const component = (item as any).component;
|
||||
const type = component.props.type;
|
||||
const addMore = !!component.props.addMore;
|
||||
|
||||
sectionsComponents.push(
|
||||
<section key={id} id={id}>
|
||||
{component}
|
||||
</section>
|
||||
);
|
||||
|
||||
if (addMore) {
|
||||
const additionalSections = getSectionsByType(sections, type);
|
||||
const elements = [];
|
||||
for (const element of additionalSections) {
|
||||
const newId = element.id;
|
||||
|
||||
const props = cloneDeep(component.props);
|
||||
props.path = 'sections.' + newId;
|
||||
props.name = element.name;
|
||||
props.isDeletable = true;
|
||||
props.addMore = false;
|
||||
props.isDuplicated = true;
|
||||
const newComponent = React.cloneElement(component, props);
|
||||
|
||||
elements.push(
|
||||
<section key={newId} id={`section-${newId}`}>
|
||||
{newComponent}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
sectionsComponents.push(...elements);
|
||||
}
|
||||
}
|
||||
|
||||
return sectionsComponents;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -68,9 +111,7 @@ const LeftSidebar = () => {
|
||||
<nav className="overflow-y-scroll">
|
||||
<div>
|
||||
<Link href="/dashboard">
|
||||
<a className="inline-flex">
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
<Logo size={40} />
|
||||
</Link>
|
||||
<Divider />
|
||||
</div>
|
||||
@ -89,7 +130,7 @@ const LeftSidebar = () => {
|
||||
|
||||
{customSections.map(({ id }) => (
|
||||
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
|
||||
<IconButton onClick={() => handleClick(id)}>
|
||||
<IconButton onClick={() => id && handleClick(id)}>
|
||||
<Star />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@ -100,15 +141,11 @@ const LeftSidebar = () => {
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{left.map(({ id, component }) => (
|
||||
<section key={id} id={id}>
|
||||
{component}
|
||||
</section>
|
||||
))}
|
||||
{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>
|
||||
))}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import { ListItem } from '@reactive-resume/schema';
|
||||
import { ListItem, Section as SectionRecord, SectionType } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@ -10,28 +10,34 @@ import Heading from '@/components/shared/Heading';
|
||||
import List from '@/components/shared/List';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { ModalName, setModalState } from '@/store/modal/modalSlice';
|
||||
import { duplicateItem } from '@/store/resume/resumeSlice';
|
||||
import { duplicateItem, duplicateSection } from '@/store/resume/resumeSlice';
|
||||
|
||||
import SectionSettings from './SectionSettings';
|
||||
|
||||
type Props = {
|
||||
path: `sections.${string}`;
|
||||
type?: SectionType;
|
||||
name?: string;
|
||||
titleKey?: string;
|
||||
subtitleKey?: string;
|
||||
isEditable?: boolean;
|
||||
isHideable?: boolean;
|
||||
isDeletable?: boolean;
|
||||
addMore?: boolean;
|
||||
isDuplicated?: boolean;
|
||||
};
|
||||
|
||||
const Section: React.FC<Props> = ({
|
||||
path,
|
||||
name = 'Section Name',
|
||||
type = 'basic',
|
||||
titleKey = 'title',
|
||||
subtitleKey = 'subtitle',
|
||||
isEditable = false,
|
||||
isHideable = false,
|
||||
isDeletable = false,
|
||||
addMore = false,
|
||||
isDuplicated = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -41,22 +47,40 @@ const Section: React.FC<Props> = ({
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const handleAdd = () => {
|
||||
const id = path.split('.')[1];
|
||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
const modal: ModalName = `builder.sections.${type}`;
|
||||
|
||||
dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
|
||||
};
|
||||
|
||||
const handleEdit = (item: ListItem) => {
|
||||
const id = path.split('.')[1];
|
||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
let modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
|
||||
const payload = validate(id) ? { path, item } : { item };
|
||||
|
||||
if (isDuplicated) {
|
||||
modal = `builder.sections.${type}`;
|
||||
payload.path = path;
|
||||
}
|
||||
|
||||
dispatch(setModalState({ modal, state: { open: true, payload } }));
|
||||
};
|
||||
|
||||
const handleDuplicate = (item: ListItem) => dispatch(duplicateItem({ path: `${path}.items`, value: item }));
|
||||
|
||||
const handleDuplicateSection = () => {
|
||||
const newSection: SectionRecord = {
|
||||
name: `${heading}`,
|
||||
type: type,
|
||||
visible: true,
|
||||
columns: 2,
|
||||
items: [],
|
||||
isDuplicated: true,
|
||||
};
|
||||
|
||||
dispatch(duplicateSection({ value: newSection, type }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} />
|
||||
@ -77,6 +101,16 @@ const Section: React.FC<Props> = ({
|
||||
{t<string>('builder.common.actions.add', { token: heading })}
|
||||
</Button>
|
||||
</footer>
|
||||
|
||||
{addMore ? (
|
||||
<div className="py-6 text-right">
|
||||
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleDuplicateSection}>
|
||||
{t<string>('builder.common.actions.duplicate')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -17,7 +17,9 @@ const CustomCSS = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {}));
|
||||
const customCSS: CustomCSSType = useAppSelector((state) =>
|
||||
get(state.resume.present, 'metadata.css', {} as CustomCSSType)
|
||||
);
|
||||
|
||||
const handleChange = (value: string | undefined) => {
|
||||
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { PictureAsPdf, Schema } from '@mui/icons-material';
|
||||
import { List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@ -45,8 +46,9 @@ const Export = () => {
|
||||
|
||||
const slug = get(resume, 'slug');
|
||||
const username = get(resume, 'user.username');
|
||||
const updatedAt = get(resume, 'updatedAt');
|
||||
|
||||
const url = await mutateAsync({ username, slug });
|
||||
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
|
||||
|
||||
download(url);
|
||||
};
|
||||
|
||||
@ -47,8 +47,8 @@ const Settings = () => {
|
||||
const id: number = useMemo(() => get(resume, 'id'), [resume]);
|
||||
const slug: string = useMemo(() => get(resume, 'slug'), [resume]);
|
||||
const username: string = useMemo(() => get(resume, 'user.username'), [resume]);
|
||||
const pageConfig: PageConfig = useMemo(() => get(resume, 'metadata.page'), [resume]);
|
||||
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]);
|
||||
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]);
|
||||
@ -98,7 +98,7 @@ const Settings = () => {
|
||||
<>
|
||||
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
<List disablePadding>
|
||||
{/* Global Settings */}
|
||||
<>
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
@ -212,7 +212,7 @@ const Settings = () => {
|
||||
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleLoadSampleData}>
|
||||
<ListItemIcon>
|
||||
<Anchor />
|
||||
@ -224,7 +224,7 @@ const Settings = () => {
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleResetResume}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever />
|
||||
|
||||
@ -31,7 +31,14 @@ const Templates = () => {
|
||||
<div key={template.id} className={styles.template}>
|
||||
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
|
||||
<ButtonBase onClick={() => handleChange(template)}>
|
||||
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
|
||||
<Image
|
||||
fill
|
||||
priority
|
||||
alt={template.name}
|
||||
src={template.preview}
|
||||
className="rounded-sm"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
|
||||
@ -16,9 +16,7 @@ type Props = {
|
||||
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(setModalState({ modal, state: { open: true } }));
|
||||
};
|
||||
const handleClick = () => dispatch(setModalState({ modal, state: { open: true } }));
|
||||
|
||||
return (
|
||||
<section className={styles.resume}>
|
||||
|
||||
@ -115,9 +115,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
}}
|
||||
>
|
||||
<ButtonBase className={styles.preview}>
|
||||
{resume.image ? (
|
||||
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
|
||||
) : null}
|
||||
{resume.image ? <Image src={resume.image} alt={resume.name} priority width={400} height={0} /> : null}
|
||||
</ButtonBase>
|
||||
</Link>
|
||||
|
||||
|
||||
@ -47,9 +47,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
<Image
|
||||
width={size}
|
||||
height={size}
|
||||
alt={user?.name}
|
||||
className={styles.avatar}
|
||||
src={getGravatarUrl(email, size)}
|
||||
alt={user?.name ?? 'User Avatar'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ type Props = {
|
||||
size?: 256 | 64 | 48 | 40 | 32;
|
||||
};
|
||||
|
||||
const Logo: React.FC<Props> = ({ size = 64 }) => {
|
||||
return <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} />;
|
||||
};
|
||||
const Logo: React.FC<Props> = ({ size = 64 }) => (
|
||||
<Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} priority />
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { isEmpty } from 'lodash';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
type Props = {
|
||||
children?: string;
|
||||
@ -12,7 +11,7 @@ const Markdown: React.FC<Props> = ({ className, children }) => {
|
||||
if (!children || isEmpty(children)) return null;
|
||||
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} className={clsx('markdown', className)}>
|
||||
<ReactMarkdown remarkPlugins={[]} className={clsx('markdown', className)}>
|
||||
{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' },
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
VolunteerActivism,
|
||||
Work,
|
||||
} from '@mui/icons-material';
|
||||
import { Section as SectionRecord } from '@reactive-resume/schema';
|
||||
import { Section as SectionRecord, SectionType } from '@reactive-resume/schema';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import Basics from '@/components/build/LeftSidebar/sections/Basics';
|
||||
@ -60,59 +60,136 @@ export const left: SidebarSection[] = [
|
||||
{
|
||||
id: 'work',
|
||||
icon: <Work />,
|
||||
component: <Section path="sections.work" titleKey="name" subtitleKey="position" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'work'}
|
||||
addMore={true}
|
||||
path="sections.work"
|
||||
titleKey="name"
|
||||
subtitleKey="position"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
icon: <School />,
|
||||
component: <Section path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'education'}
|
||||
path="sections.education"
|
||||
titleKey="institution"
|
||||
subtitleKey="area"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'awards',
|
||||
icon: <EmojiEvents />,
|
||||
component: <Section path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'awards'} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'certifications',
|
||||
icon: <CardGiftcard />,
|
||||
component: <Section path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'certifications'}
|
||||
path="sections.certifications"
|
||||
titleKey="name"
|
||||
subtitleKey="issuer"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'publications',
|
||||
icon: <MenuBook />,
|
||||
component: <Section path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'publications'}
|
||||
path="sections.publications"
|
||||
titleKey="name"
|
||||
subtitleKey="publisher"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
icon: <Architecture />,
|
||||
component: <Section path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'skills'} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'languages',
|
||||
icon: <Language />,
|
||||
component: <Section path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'languages'} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'interests',
|
||||
icon: <Sailing />,
|
||||
component: <Section path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'interests'}
|
||||
path="sections.interests"
|
||||
titleKey="name"
|
||||
subtitleKey="keywords"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'volunteer',
|
||||
icon: <VolunteerActivism />,
|
||||
component: (
|
||||
<Section path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable />
|
||||
<Section
|
||||
type={'volunteer'}
|
||||
path="sections.volunteer"
|
||||
titleKey="organization"
|
||||
subtitleKey="position"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
icon: <Coffee />,
|
||||
component: <Section path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'projects'}
|
||||
path="sections.projects"
|
||||
titleKey="name"
|
||||
subtitleKey="description"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'references',
|
||||
icon: <Groups />,
|
||||
component: <Section path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'references'}
|
||||
path="sections.references"
|
||||
titleKey="name"
|
||||
subtitleKey="relationship"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -164,7 +241,19 @@ export const right: SidebarSection[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => {
|
||||
export const getSectionsByType = (sections: Record<string, SectionRecord>, type: SectionType): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
if (section.type.startsWith(type) && section.isDuplicated) {
|
||||
return [...acc, { ...section, id }];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
export const getCustomSections = (sections: Record<string, SectionRecord>): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
@ -173,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as Array<Required<SectionRecord>>);
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
const sections = [...left, ...right];
|
||||
|
||||
@ -169,7 +169,8 @@ 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 <a onClick={handleRecoverAccount}>recover your account here.</a>
|
||||
In case you have forgotten your password, you can
|
||||
<a onClick={handleRecoverAccount}>recover your account here.</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</BaseModal>
|
||||
|
||||
@ -60,13 +60,14 @@ const CustomModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
|
||||
|
||||
const path: string = get(payload, 'path', '');
|
||||
const path: string = get(payload, 'path', 'sections.custom');
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
|
||||
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
|
||||
@ -260,9 +261,9 @@ const CustomModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@ -2,7 +2,7 @@ import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
|
||||
import { WorkExperience } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -20,8 +20,6 @@ import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||
|
||||
type FormData = WorkExperience;
|
||||
|
||||
const path: SectionPath = 'sections.work';
|
||||
|
||||
const defaultState: FormData = {
|
||||
name: '',
|
||||
position: '',
|
||||
@ -50,10 +48,12 @@ const WorkModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.work']);
|
||||
const path: string = get(payload, 'path', 'sections.work');
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
@ -77,7 +77,7 @@ const WorkModal: React.FC = () => {
|
||||
const handleClose = () => {
|
||||
dispatch(
|
||||
setModalState({
|
||||
modal: `builder.${path}`,
|
||||
modal: 'builder.sections.work',
|
||||
state: { open: false },
|
||||
})
|
||||
);
|
||||
|
||||
@ -10,75 +10,74 @@
|
||||
"dependencies": {
|
||||
"@beam-australia/react-env": "^3.1.1",
|
||||
"@date-io/dayjs": "^2.16.0",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@emotion/css": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@hello-pangea/dnd": "^16.0.1",
|
||||
"@hookform/resolvers": "2.9.9",
|
||||
"@hookform/resolvers": "2.9.10",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/lab": "^5.0.0-alpha.103",
|
||||
"@mui/material": "^5.10.9",
|
||||
"@mui/system": "^5.10.9",
|
||||
"@mui/x-date-pickers": "5.0.4",
|
||||
"@next/env": "^12.3.1",
|
||||
"@react-oauth/google": "^0.2.8",
|
||||
"@reduxjs/toolkit": "^1.8.6",
|
||||
"axios": "^1.1.2",
|
||||
"@mui/icons-material": "^5.10.15",
|
||||
"@mui/lab": "^5.0.0-alpha.109",
|
||||
"@mui/material": "^5.10.15",
|
||||
"@mui/system": "^5.10.15",
|
||||
"@mui/x-date-pickers": "5.0.8",
|
||||
"@next/env": "^13.0.5",
|
||||
"@react-oauth/google": "^0.5.0",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
"axios": "^1.2.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.5",
|
||||
"dayjs": "^1.11.6",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.6.3",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "12.3.1",
|
||||
"next-i18next": "^12.1.0",
|
||||
"next": "13.0.5",
|
||||
"next-i18next": "^13.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.37.0",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-icons": "^4.6.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-query": "^3.39.2",
|
||||
"react-redux": "^8.0.4",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.2.1",
|
||||
"redux-undo": "^1.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.1",
|
||||
"sharp": "^0.31.2",
|
||||
"uuid": "^9.0.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@babel/core": "^7.20.2",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/lodash": "^4.14.190",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webfontloader": "^1.6.35",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint-config-next": "^12.3.1",
|
||||
"eslint-plugin-tailwindcss": "^3.6.2",
|
||||
"next-sitemap": "^3.1.25",
|
||||
"postcss": "^8.4.18",
|
||||
"sass": "^1.55.0",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.8.4"
|
||||
"eslint-config-next": "^13.0.5",
|
||||
"eslint-plugin-tailwindcss": "^3.7.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"next-sitemap": "^3.1.32",
|
||||
"postcss": "^8.4.19",
|
||||
"sass": "^1.56.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -60,10 +61,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]);
|
||||
|
||||
@ -96,7 +99,9 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({ username, slug });
|
||||
const updatedAt = get(resume, 'updatedAt');
|
||||
|
||||
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -17,13 +17,11 @@ import { fetchResumes } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import styles from '@/styles/pages/Dashboard.module.scss';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
|
||||
},
|
||||
});
|
||||
|
||||
const Dashboard: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -48,9 +46,7 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
<Logo size={40} />
|
||||
</Link>
|
||||
|
||||
<Avatar size={40} />
|
||||
@ -58,15 +54,15 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<main className={styles.resumes}>
|
||||
<ResumeCard
|
||||
modal="dashboard.create-resume"
|
||||
icon={Add}
|
||||
modal="dashboard.create-resume"
|
||||
title={t<string>('dashboard.create-resume.title')}
|
||||
subtitle={t<string>('dashboard.create-resume.subtitle')}
|
||||
/>
|
||||
|
||||
<ResumeCard
|
||||
modal="dashboard.import-external"
|
||||
icon={ImportExport}
|
||||
modal="dashboard.import-external"
|
||||
title={t<string>('dashboard.import-external.title')}
|
||||
subtitle={t<string>('dashboard.import-external.subtitle')}
|
||||
/>
|
||||
|
||||
@ -22,13 +22,11 @@ import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
|
||||
},
|
||||
});
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -39,11 +37,8 @@ const Home: NextPage = () => {
|
||||
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
|
||||
|
||||
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||
|
||||
const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', state: { open: true } }));
|
||||
|
||||
const handleToggle = () => dispatch(setTheme({ theme: theme === 'light' ? 'dark' : 'light' }));
|
||||
|
||||
const handleLogout = () => dispatch(logout());
|
||||
|
||||
return (
|
||||
@ -117,7 +112,13 @@ const Home: NextPage = () => {
|
||||
<div className={styles.screenshots}>
|
||||
{screenshots.map(({ src, alt }) => (
|
||||
<a key={src} href={src} className={styles.image} target="_blank" rel="noreferrer">
|
||||
<Image src={src} alt={alt} layout="fill" objectFit="cover" />
|
||||
<Image
|
||||
fill
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@ -186,7 +187,13 @@ const Home: NextPage = () => {
|
||||
|
||||
<section className={styles.section}>
|
||||
<a href={DIGITALOCEAN_URL} target="_blank" rel="noreferrer">
|
||||
<Image src={`/images/sponsors/${theme=="dark"?"digitalocean":"digitaloceanLight"}.svg`} alt="Powered By DigitalOcean" width={200} height={40} />
|
||||
<Image
|
||||
src={`/images/sponsors/${theme == 'dark' ? 'digitalocean' : 'digitaloceanLight'}.svg`}
|
||||
style={{ width: 200, height: 40, objectFit: 'contain' }}
|
||||
alt="Powered By DigitalOcean"
|
||||
width={200}
|
||||
height={40}
|
||||
/>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -69,7 +70,11 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({ username: resume.user.username, slug: resume.slug });
|
||||
const url = await mutateAsync({
|
||||
username: resume.user.username,
|
||||
slug: resume.slug,
|
||||
lastUpdated: dayjs(resume.updatedAt).unix().toString(),
|
||||
});
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
|
||||
@ -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": "Papier größe",
|
||||
"secondary": "Legt die Abmessungen Ihrer Lebenslaufseiten fest"
|
||||
"primary": "Papiergröße",
|
||||
"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.",
|
||||
"title": "Reaktives Lebenslauf",
|
||||
"subtitle": "Ein kostenloser Open Source Lebenslauf-Baukasten.",
|
||||
"title": "Reactive Resume",
|
||||
"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,7 +3,8 @@
|
||||
"actions": {
|
||||
"add": "Add New {{token}}",
|
||||
"delete": "Delete {{token}}",
|
||||
"edit": "Edit {{token}}"
|
||||
"edit": "Edit {{token}}",
|
||||
"duplicate": "Duplicate Section"
|
||||
},
|
||||
"columns": {
|
||||
"heading": "Columns",
|
||||
@ -244,7 +245,7 @@
|
||||
"pdf": {
|
||||
"loading": {
|
||||
"primary": "Generating PDF",
|
||||
"secondary": "Please wait as your PDF gets generated, this may take upto 15 seconds."
|
||||
"secondary": "Please wait as your PDF gets generated, this may take up to 15 seconds."
|
||||
},
|
||||
"normal": {
|
||||
"primary": "PDF",
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"add": "Új hozzáadása {{token}}",
|
||||
"add": "Új {{token}} hozzáadása",
|
||||
"delete": "{{token}} törlése",
|
||||
"edit": "{{token}} szerkesztése"
|
||||
},
|
||||
@ -20,7 +20,7 @@
|
||||
"label": "E-mail cím"
|
||||
},
|
||||
"end-date": {
|
||||
"help-text": "Hagyja üresen ezt a mezőt, ha még mindig tart van",
|
||||
"help-text": "Hagyja üresen ezt a mezőt, ha még folyamatban van",
|
||||
"label": "Befejezés dátuma"
|
||||
},
|
||||
"keywords": {
|
||||
@ -51,7 +51,7 @@
|
||||
"label": "Összegzés"
|
||||
},
|
||||
"title": {
|
||||
"label": "Cím"
|
||||
"label": "Titulus"
|
||||
},
|
||||
"url": {
|
||||
"label": "Honlap"
|
||||
@ -63,30 +63,30 @@
|
||||
"list": {
|
||||
"actions": {
|
||||
"delete": "Törlés",
|
||||
"duplicate": "Duplikálás",
|
||||
"duplicate": "Másolás",
|
||||
"edit": "Szerkesztés"
|
||||
},
|
||||
"empty-text": "Ez a lista üres."
|
||||
},
|
||||
"tooltip": {
|
||||
"delete-item": "Biztosan törli ezt az elemet? Ez egy visszafordíthatatlan művelet.",
|
||||
"delete-item": "Biztosan törli ezt az elemet? Ez a művelet nem visszavonható.",
|
||||
"delete-section": "Szakasz törlése",
|
||||
"rename-section": "Szakasz átnevezése",
|
||||
"toggle-visibility": "Láthatóság váltása"
|
||||
"toggle-visibility": "Láthatóság ki/be"
|
||||
}
|
||||
},
|
||||
"controller": {
|
||||
"tooltip": {
|
||||
"center-artboard": "Központi rajztábla",
|
||||
"copy-link": "Link másolása az önéletrajzba",
|
||||
"copy-link": "Önéletrajz link másolása",
|
||||
"export-pdf": "Exportálás PDF-be",
|
||||
"toggle-orientation": "Oldaltájolás váltása",
|
||||
"toggle-page-break-line": "Oldaltörés vonal váltása",
|
||||
"toggle-sidebars": "Az oldalsávok váltása",
|
||||
"toggle-orientation": "Oldaltájolás",
|
||||
"toggle-page-break-line": "Oldaltörés vonal ki/be",
|
||||
"toggle-sidebars": "Oldalsávok ki/be",
|
||||
"zoom-in": "Nagyítás",
|
||||
"zoom-out": "Kicsinyítés",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
"undo": "Visszavonás",
|
||||
"redo": "Mégis"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -96,7 +96,7 @@
|
||||
"rename": "Átnevezés",
|
||||
"share-link": "Link megosztása",
|
||||
"tooltips": {
|
||||
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez egy visszafordíthatatlan művelet.",
|
||||
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez a művelet nem visszavonható.",
|
||||
"share-link": "Az önéletrajz láthatóságát nyilvánosra kell változtatnia, hogy mások számára is látható legyen."
|
||||
}
|
||||
}
|
||||
@ -138,7 +138,7 @@
|
||||
"heading": "Alak"
|
||||
},
|
||||
"size": {
|
||||
"heading": "Méret (px-ben)"
|
||||
"heading": "Méret (pixel)"
|
||||
}
|
||||
},
|
||||
"photo-upload": {
|
||||
@ -189,7 +189,7 @@
|
||||
"label": "Irányítószám"
|
||||
},
|
||||
"region": {
|
||||
"label": "Vidék"
|
||||
"label": "Régió"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
@ -248,7 +248,7 @@
|
||||
},
|
||||
"normal": {
|
||||
"primary": "PDF",
|
||||
"secondary": "Töltse le önéletrajzának PDF formátumát, amelyet kinyomtathat és elküldhet álmai munkahelyére. Ez a fájl nem importálható vissza további szerkesztéshez."
|
||||
"secondary": "Töltse le önéletrajzát PDF formátumban, amelyet kinyomtathat és elküldhet álmai munkahelyére. Ez a fájl nem importálható vissza további szerkesztéshez."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -265,7 +265,7 @@
|
||||
"heading": "Hibák? Funkciókérés?"
|
||||
},
|
||||
"donate": {
|
||||
"body": "Ha tetszett a Reactive Resume, kérjük, fontolja meg, hogy amennyit csak tud, adományozzon arra, hogy az alkalmazás folyamatosan működjön, hirdetések nélkül és örökké ingyenesen.",
|
||||
"body": "Ha elégedett a Reactive Resume alkalmazással, kérjük, fontolja meg, hogy tetszőleges összeggel támogassa munkánkat, hogy továbbra is ingyenes és hirdetésmentes lehessen.",
|
||||
"button": "Vegyél nekem egy kávét",
|
||||
"heading": "Adományozzon a Reactive Resume-nak"
|
||||
},
|
||||
@ -277,12 +277,12 @@
|
||||
"global": {
|
||||
"date": {
|
||||
"primary": "Dátum",
|
||||
"secondary": "Az alkalmazásban használható dátumformátum"
|
||||
"secondary": "Az alkalmazásban használt dátumformátum"
|
||||
},
|
||||
"heading": "Globális",
|
||||
"language": {
|
||||
"primary": "Nyelv",
|
||||
"secondary": "Az alkalmazásban használható megjelenítési nyelv"
|
||||
"secondary": "Az alkalmazásban használt megjelenítési nyelv"
|
||||
},
|
||||
"theme": {
|
||||
"primary": "Téma"
|
||||
@ -292,7 +292,7 @@
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "Papírméret",
|
||||
"secondary": "Meghatározza az önéletrajzi oldalak méreteit"
|
||||
"secondary": "Meghatározza az önéletrajz oldalméreteit"
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "Törésvonal",
|
||||
@ -301,7 +301,7 @@
|
||||
"heading": "oldal",
|
||||
"orientation": {
|
||||
"disabled": "Nincs hatása, ha csak egy oldal van",
|
||||
"primary": "Irányultság",
|
||||
"primary": "Tájolás",
|
||||
"secondary": "Az oldalak vízszintes vagy függőleges megjelenítése"
|
||||
}
|
||||
},
|
||||
@ -320,7 +320,7 @@
|
||||
"sharing": {
|
||||
"heading": "Megosztás",
|
||||
"short-url": {
|
||||
"label": "Rövid URL-t részesítsen előnyben"
|
||||
"label": "Rövid URL előnyben részesítése"
|
||||
},
|
||||
"visibility": {
|
||||
"subtitle": "A link birtokában bárki megtekintheti önéletrajzát",
|
||||
@ -336,7 +336,7 @@
|
||||
"label": "Háttér"
|
||||
},
|
||||
"primary": {
|
||||
"label": "Elsődleges"
|
||||
"label": "Elsődleges "
|
||||
},
|
||||
"text": {
|
||||
"label": "Szöveg"
|
||||
@ -356,7 +356,7 @@
|
||||
"heading": "Tipográfia",
|
||||
"widgets": {
|
||||
"body": {
|
||||
"label": "Test"
|
||||
"label": "Szövegtörzs"
|
||||
},
|
||||
"headings": {
|
||||
"label": "Címsorok"
|
||||
|
||||
@ -6,14 +6,14 @@
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"credit": "<1>Amruth Pillai szenvedélyes projektje</1>",
|
||||
"credit": "<1>Amruth Pillai hobbi projektje</1>",
|
||||
"license": "A közösség által, a közösségért."
|
||||
},
|
||||
"markdown": {
|
||||
"help-text": "Ez a szakasz támogatja a <1>markdown</1> formázást."
|
||||
},
|
||||
"date": {
|
||||
"present": "Ajándék"
|
||||
"present": "Jelenleg is"
|
||||
},
|
||||
"subtitle": "Ingyenes és nyílt forráskódú önéletrajzkészítő.",
|
||||
"title": "Reactive Resume",
|
||||
|
||||
@ -4,18 +4,18 @@
|
||||
"title": "Új önéletrajz létrehozása"
|
||||
},
|
||||
"import-external": {
|
||||
"subtitle": "LinkedIn, JSON önéletrajz, reaktív önéletrajz",
|
||||
"subtitle": "LinkedIn, JSON önéletrajz, Reactive Resume",
|
||||
"title": "Importálás külső forrásokból"
|
||||
},
|
||||
"resume": {
|
||||
"menu": {
|
||||
"delete": "Törlés",
|
||||
"duplicate": "Másolat",
|
||||
"open": "Nyisd ki",
|
||||
"open": "Megnyitás",
|
||||
"rename": "Átnevezés",
|
||||
"share-link": "Link megosztása",
|
||||
"tooltips": {
|
||||
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez egy visszafordíthatatlan művelet.",
|
||||
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez a művelet nem visszavonható.",
|
||||
"share-link": "Az önéletrajz láthatóságát nyilvánosra kell változtatnia, hogy mások számára is látható legyen."
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"actions": {
|
||||
"app": "Lépjen az App",
|
||||
"app": "Alkalmazás indítása",
|
||||
"login": "Belépés",
|
||||
"logout": "Kijelentkezés",
|
||||
"register": "Regisztráció"
|
||||
@ -8,7 +8,7 @@
|
||||
"features": {
|
||||
"heading": "Jellemzők",
|
||||
"list": {
|
||||
"ads": "Nincs reklám",
|
||||
"ads": "Reklámmentes",
|
||||
"export": "Exportálja önéletrajzát JSON vagy PDF formátumba",
|
||||
"free": "Ingyenes, örökre",
|
||||
"import": "Adatok importálása a LinkedInből, JSON Resume",
|
||||
@ -20,11 +20,11 @@
|
||||
"links": {
|
||||
"heading": "Linkek",
|
||||
"links": {
|
||||
"donate": "Adományoz",
|
||||
"donate": "Adományozás",
|
||||
"github": "Forráskód",
|
||||
"docs": "Dokumentáció",
|
||||
"privacy": "Adatvédelmi irányelvek",
|
||||
"service": "Szolgáltatási feltételek"
|
||||
"service": "Felhasználói feltételek"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"auth": {
|
||||
"forgot-password": {
|
||||
"actions": {
|
||||
"send-email": "Jelszó visszaállítása e-mail küldése"
|
||||
"send-email": "Jelszó helyreállító e-mail küldése"
|
||||
},
|
||||
"body": "Csak adja meg a helyreállítani kívánt fiókhoz társított e-mail címet.",
|
||||
"form": {
|
||||
@ -27,14 +27,14 @@
|
||||
"label": "Felhasználónév"
|
||||
}
|
||||
},
|
||||
"heading": "Jelentkezz be a fiókodba",
|
||||
"heading": "Jelentkezzen be a fiókjába",
|
||||
"recover-text": "Ha elfelejtette jelszavát, <1>visszaállíthatja fiókját</1> itt.",
|
||||
"register-text": "Ha nem rendelkezik fiókkal, <1>létrehozhat egy fiókot</1> itt."
|
||||
},
|
||||
"register": {
|
||||
"actions": {
|
||||
"register": "Regisztráció",
|
||||
"google": "Regisztráljon a Google-nál"
|
||||
"google": "Regisztráljon Google fiókkal"
|
||||
},
|
||||
"body": "Kérjük, adja meg személyes adatait fiók létrehozásához.",
|
||||
"form": {
|
||||
@ -87,7 +87,7 @@
|
||||
"label": "Nyilvánosan elérhető?"
|
||||
},
|
||||
"slug": {
|
||||
"label": "Meztelen csiga"
|
||||
"label": "Saját URL"
|
||||
}
|
||||
},
|
||||
"heading": "Új önéletrajz létrehozása"
|
||||
@ -126,7 +126,7 @@
|
||||
"label": "Név"
|
||||
},
|
||||
"slug": {
|
||||
"label": "Meztelen csiga"
|
||||
"label": "Saját URL"
|
||||
}
|
||||
},
|
||||
"heading": "Nevezze át önéletrajzát"
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"label": "Tanggal"
|
||||
},
|
||||
"description": {
|
||||
"label": "Keterangan"
|
||||
"label": "Deskripsi"
|
||||
},
|
||||
"email": {
|
||||
"label": "Alamat Email"
|
||||
@ -130,7 +130,7 @@
|
||||
"label": "Batas"
|
||||
},
|
||||
"grayscale": {
|
||||
"label": "Grayscale"
|
||||
"label": "Tingkat keabuan"
|
||||
},
|
||||
"heading": "Efek"
|
||||
},
|
||||
@ -138,7 +138,7 @@
|
||||
"heading": "Bentuk"
|
||||
},
|
||||
"size": {
|
||||
"heading": "Besar (dalam px)"
|
||||
"heading": "Ukuran (dalam px)"
|
||||
}
|
||||
},
|
||||
"photo-upload": {
|
||||
@ -158,7 +158,7 @@
|
||||
"education": {
|
||||
"form": {
|
||||
"area-study": {
|
||||
"label": "Area belajar"
|
||||
"label": "Bidang Studi"
|
||||
},
|
||||
"courses": {
|
||||
"label": "Kursus"
|
||||
@ -170,7 +170,7 @@
|
||||
"label": "Tingkatan"
|
||||
},
|
||||
"institution": {
|
||||
"label": "Lembaga"
|
||||
"label": "Institusi"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"license": "Door de gemeenschap, voor de gemeenschap."
|
||||
},
|
||||
"markdown": {
|
||||
"help-text": "Deze sectie ondersteunt <1>html</1> opmaak."
|
||||
"help-text": "Deze sectie ondersteunt <1>markdown</1> opmaak."
|
||||
},
|
||||
"date": {
|
||||
"present": "Heden"
|
||||
@ -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."
|
||||
|
||||
@ -18,8 +18,8 @@ export type LoginWithGoogleParams = {
|
||||
|
||||
export type RegisterParams = {
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ const axios = _axios.create({ baseURL });
|
||||
axios.interceptors.request.use((config) => {
|
||||
const { accessToken } = store.getState().auth;
|
||||
|
||||
// @ts-ignore
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
|
||||
@ -3,7 +3,12 @@ import axios from './axios';
|
||||
export type PrintResumeAsPdfParams = {
|
||||
username: string;
|
||||
slug: string;
|
||||
lastUpdated: string;
|
||||
};
|
||||
|
||||
export const printResumeAsPdf = (printResumeAsPdfParams: PrintResumeAsPdfParams): Promise<string> =>
|
||||
axios.get(`/printer/${printResumeAsPdfParams.username}/${printResumeAsPdfParams.slug}`).then((res) => res.data);
|
||||
axios
|
||||
.get(
|
||||
`/printer/${printResumeAsPdfParams.username}/${printResumeAsPdfParams.slug}?lastUpdated=${printResumeAsPdfParams.lastUpdated}`
|
||||
)
|
||||
.then((res) => res.data);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -34,7 +34,7 @@ const store = configureStore({
|
||||
},
|
||||
});
|
||||
|
||||
sagaMiddleware.run(syncSaga);
|
||||
sagaMiddleware.run(() => syncSaga(store.dispatch));
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ export type ModalName =
|
||||
| 'dashboard.import-external'
|
||||
| 'dashboard.rename-resume'
|
||||
| 'builder.sections.profile'
|
||||
| 'builder.sections.work'
|
||||
| `builder.sections.${string}`;
|
||||
|
||||
export type ModalState = {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ListItem, Profile, Resume, Section } from '@reactive-resume/schema';
|
||||
import { ListItem, Profile, Resume, Section, SectionType } from '@reactive-resume/schema';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
@ -7,6 +7,8 @@ import pick from 'lodash/pick';
|
||||
import set from 'lodash/set';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getSectionsByType } from '@/config/sections';
|
||||
|
||||
type SetResumeStatePayload = { path: string; value: unknown };
|
||||
|
||||
type AddItemPayload = { path: string; value: ListItem };
|
||||
@ -17,7 +19,7 @@ type DuplicateItemPayload = { path: string; value: ListItem };
|
||||
|
||||
type DeleteItemPayload = { path: string; value: ListItem };
|
||||
|
||||
type AddSectionPayload = { value: Section };
|
||||
type AddSectionPayload = { value: Section; type: SectionType };
|
||||
|
||||
type DeleteSectionPayload = { path: string };
|
||||
|
||||
@ -38,7 +40,7 @@ export const resumeSlice = createSlice({
|
||||
addItem: (state: Resume, action: PayloadAction<AddItemPayload>) => {
|
||||
const { path, value } = action.payload;
|
||||
const id = uuidv4();
|
||||
const list = get(state, path, []);
|
||||
const list: ListItem[] = get(state, path, []);
|
||||
const item = merge(value, { id });
|
||||
|
||||
list.push(item);
|
||||
@ -80,6 +82,15 @@ export const resumeSlice = createSlice({
|
||||
state.sections[id] = value;
|
||||
state.metadata.layout[0][0].push(id);
|
||||
},
|
||||
duplicateSection: (state: Resume, action: PayloadAction<AddSectionPayload>) => {
|
||||
const { value, type } = action.payload;
|
||||
|
||||
const id = getSectionsByType(state.sections, type).length + 1;
|
||||
value.name = value.name + '-' + id;
|
||||
|
||||
state.sections[`${type}-${id}`] = value;
|
||||
state.metadata.layout[0][0].push(`${type}-${id}`);
|
||||
},
|
||||
deleteSection: (state: Resume, action: PayloadAction<DeleteSectionPayload>) => {
|
||||
const { path } = action.payload;
|
||||
const id = path.split('.')[1];
|
||||
@ -119,6 +130,7 @@ export const {
|
||||
duplicateItem,
|
||||
deleteItem,
|
||||
addSection,
|
||||
duplicateSection,
|
||||
deleteSection,
|
||||
addPage,
|
||||
deletePage,
|
||||
|
||||
@ -3,7 +3,7 @@ import debounce from 'lodash/debounce';
|
||||
import { select, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
import { updateResume } from '@/services/resume';
|
||||
import { RootState } from '@/store/index';
|
||||
import { AppDispatch, RootState } from '@/store/index';
|
||||
|
||||
import {
|
||||
addItem,
|
||||
@ -12,23 +12,26 @@ import {
|
||||
deleteSection,
|
||||
duplicateItem,
|
||||
editItem,
|
||||
setResume,
|
||||
setResumeState,
|
||||
} from '../resume/resumeSlice';
|
||||
|
||||
const DEBOUNCE_WAIT = 1000;
|
||||
|
||||
const debouncedSync = debounce((resume: Resume) => updateResume(resume), DEBOUNCE_WAIT);
|
||||
const debouncedSync = debounce(
|
||||
(resume: Resume, dispatch: AppDispatch) => updateResume(resume).then((resume) => dispatch(setResume(resume))),
|
||||
DEBOUNCE_WAIT
|
||||
);
|
||||
|
||||
function* handleSync() {
|
||||
function* handleSync(dispatch: AppDispatch) {
|
||||
const resume: Resume = yield select((state: RootState) => state.resume.present);
|
||||
|
||||
debouncedSync(resume);
|
||||
debouncedSync(resume, dispatch);
|
||||
}
|
||||
|
||||
function* syncSaga() {
|
||||
yield takeLatest(
|
||||
[setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection],
|
||||
handleSync
|
||||
function* syncSaga(dispatch: AppDispatch) {
|
||||
yield takeLatest([setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection], () =>
|
||||
handleSync(dispatch)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -22,11 +22,19 @@
|
||||
}
|
||||
|
||||
p {
|
||||
@apply leading-relaxed;
|
||||
@apply leading-normal;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply cursor-pointer font-medium hover:underline;
|
||||
@apply cursor-pointer font-medium;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
@apply grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6;
|
||||
|
||||
.image {
|
||||
@apply relative h-64 rounded hover:opacity-75;
|
||||
@apply relative h-48 rounded hover:opacity-75;
|
||||
@apply border-2 dark:border-neutral-700;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ const Castform: React.FC<PageProps> = ({ page }) => {
|
||||
const isFirstPage = useMemo(() => page === 0, [page]);
|
||||
|
||||
const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
@ -6,7 +6,7 @@ import { useMemo } from 'react';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]);
|
||||
|
||||
return (
|
||||
|
||||
@ -19,7 +19,7 @@ export const MastheadSidebar: React.FC = () => {
|
||||
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
|
||||
(state) => state.resume.present.basics
|
||||
);
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
@ -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';
|
||||
@ -21,10 +22,12 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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) => {
|
||||
@ -44,13 +47,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
@ -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">
|
||||
|
||||
@ -18,7 +18,7 @@ const Gengar: React.FC<PageProps> = ({ page }) => {
|
||||
const isFirstPage = useMemo(() => page === 0, [page]);
|
||||
|
||||
const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
const backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]);
|
||||
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
return (
|
||||
<h3
|
||||
|
||||
@ -20,7 +20,7 @@ export const MastheadSidebar: React.FC = () => {
|
||||
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
|
||||
(state) => state.resume.present.basics
|
||||
);
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
const iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const BadgeDisplay: React.FC<Props> = ({ items }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
|
||||
if (!isArray(items) || isEmpty(items)) return null;
|
||||
@ -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>
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
return (
|
||||
<h3
|
||||
|
||||
@ -22,7 +22,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -46,13 +46,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -12,7 +12,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const BadgeDisplay: React.FC<Props> = ({ items }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
|
||||
if (!isArray(items) || isEmpty(items)) return null;
|
||||
@ -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';
|
||||
@ -21,7 +22,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +46,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
@ -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;
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
return (
|
||||
<h2
|
||||
|
||||
@ -16,7 +16,7 @@ const Masthead: React.FC = () => {
|
||||
const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector(
|
||||
(state) => state.resume.present.basics
|
||||
);
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} className="mb-2 grid gap-1">
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
return (
|
||||
<h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -4,7 +4,7 @@ import get from 'lodash/get';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
|
||||
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
|
||||
return (
|
||||
<h3
|
||||
|
||||
@ -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';
|
||||
@ -62,7 +63,7 @@ export const MastheadSidebar: React.FC = () => {
|
||||
};
|
||||
|
||||
export const MastheadMain: React.FC = () => {
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
|
||||
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
|
||||
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
|
||||
|
||||
const { name, summary, headline } = useAppSelector((state) => state.resume.present.basics);
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
headlinePath = 'headline',
|
||||
keywordsPath = 'keywords',
|
||||
}) => {
|
||||
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
|
||||
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 primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
|
||||
|
||||
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
|
||||
subtitle = parseListItemPath(item, subtitlePath),
|
||||
headline = parseListItemPath(item, headlinePath),
|
||||
keywords: string[] = get(item, keywordsPath),
|
||||
url: string = get(item, 'url'),
|
||||
summary: string = get(item, 'summary'),
|
||||
level: string = get(item, 'level'),
|
||||
levelNum: number = get(item, 'levelNum'),
|
||||
phone: string = get(item, 'phone'),
|
||||
email: string = get(item, 'email'),
|
||||
date = formatDateString(get(item, 'date'), dateFormat);
|
||||
url: string = get(item, 'url', ''),
|
||||
level: string = get(item, 'level', ''),
|
||||
phone: string = get(item, 'phone', ''),
|
||||
email: string = get(item, 'email', ''),
|
||||
summary: string = get(item, 'summary', ''),
|
||||
levelNum: number = get(item, 'levelNum', 0),
|
||||
date = formatDateString(get(item, 'date', ''), dateFormat);
|
||||
|
||||
return (
|
||||
<div key={id} id={id} className="grid gap-1">
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { find } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import React from 'react';
|
||||
import { validate } from 'uuid';
|
||||
@ -44,11 +45,21 @@ const sectionMap = (Section: React.FC<SectionProps>): Record<string, JSX.Element
|
||||
});
|
||||
|
||||
export const getSectionById = (id: string, Section: React.FC<SectionProps>): JSX.Element => {
|
||||
// Check if section id is a custom section (an uuid)
|
||||
if (validate(id)) {
|
||||
return <Section key={id} path={`sections.${id}`} />;
|
||||
}
|
||||
|
||||
return get(sectionMap(Section), id);
|
||||
// Check if section id is a predefined seciton in config
|
||||
const predefinedSection = get(sectionMap(Section), id);
|
||||
|
||||
if (predefinedSection) {
|
||||
return predefinedSection;
|
||||
}
|
||||
|
||||
// Other ways section should be a cloned section
|
||||
const section = find(sectionMap(Section), (element, key) => id.includes(key));
|
||||
return React.cloneElement(section!, { path: `sections.${id}` });
|
||||
};
|
||||
|
||||
export default sectionMap;
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
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={clsx('underline underline-offset-2', textClassName)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</div>
|
||||
@ -24,7 +32,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',
|
||||
@ -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(date).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(date.start).format(formatStr);
|
||||
}
|
||||
|
||||
return `${dayjs(date.start).utc(true).format(formatStr)} - ${presentString}`;
|
||||
if (!isEmpty(date.end) && dayjs(date.end).isValid()) {
|
||||
return `${dayjs(date.start).format(formatStr)} - ${dayjs(date.end).format(formatStr)}`;
|
||||
}
|
||||
|
||||
return `${dayjs(date.start).format(formatStr)} - ${presentString}`;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -9,9 +9,9 @@ const FontWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) =
|
||||
|
||||
const loadFonts = useCallback(async () => {
|
||||
const WebFont = (await import('webfontloader')).default;
|
||||
const families = Object.values<string[]>(typography.family).reduce(
|
||||
const families = Object.values(typography.family).reduce(
|
||||
(acc, family) => [...acc, `${family}:400,600,700`],
|
||||
[]
|
||||
[] as string[]
|
||||
);
|
||||
|
||||
WebFont.load({ google: { families } });
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { toggleSidebar } from '@/store/build/buildSlice';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
|
||||
const HotkeysWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useHotkeys('ctrl+/, cmd+/', () => {
|
||||
dispatch(toggleSidebar({ sidebar: 'left' }));
|
||||
dispatch(toggleSidebar({ sidebar: 'right' }));
|
||||
});
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default HotkeysWrapper;
|
||||
@ -1,17 +1,14 @@
|
||||
import DateWrapper from './DateWrapper';
|
||||
import FontWrapper from './FontWrapper';
|
||||
import HotkeysWrapper from './HotkeysWrapper';
|
||||
import ThemeWrapper from './ThemeWrapper';
|
||||
|
||||
const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return (
|
||||
<ThemeWrapper>
|
||||
<FontWrapper>
|
||||
<HotkeysWrapper>
|
||||
<DateWrapper>
|
||||
<>{children}</>
|
||||
</DateWrapper>
|
||||
</HotkeysWrapper>
|
||||
<DateWrapper>
|
||||
<>{children}</>
|
||||
</DateWrapper>
|
||||
</FontWrapper>
|
||||
</ThemeWrapper>
|
||||
);
|
||||
|
||||
@ -3,7 +3,6 @@ version: "3.8"
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:alpine
|
||||
container_name: postgres
|
||||
restart: always
|
||||
ports:
|
||||
- 5432:5432
|
||||
@ -25,7 +24,6 @@ services:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./server/Dockerfile
|
||||
container_name: server
|
||||
restart: always
|
||||
ports:
|
||||
- 3100:3100
|
||||
@ -58,13 +56,13 @@ services:
|
||||
- STORAGE_URL_PREFIX=
|
||||
- STORAGE_ACCESS_KEY=
|
||||
- STORAGE_SECRET_KEY=
|
||||
- PDF_DELETION_TIME=
|
||||
|
||||
client:
|
||||
image: amruthpillai/reactive-resume:client-latest
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./client/Dockerfile
|
||||
container_name: client
|
||||
restart: always
|
||||
ports:
|
||||
- 3000:3000
|
||||
@ -76,4 +74,4 @@ services:
|
||||
- PUBLIC_GOOGLE_CLIENT_ID=
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
pgdata:
|
||||
18
package.json
18
package.json
@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "reactive-resume",
|
||||
"version": "3.6.8",
|
||||
"version": "3.6.14",
|
||||
"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",
|
||||
"format": "prettier --write .",
|
||||
"release": "standard-version --release-as patch"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"workspaces": [
|
||||
"schema",
|
||||
@ -17,17 +16,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"env-cmd": "^10.1.0",
|
||||
"turbo": "^1.5.6"
|
||||
"turbo": "^1.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"eslint": "^8.25.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/parser": "^5.44.0",
|
||||
"eslint": "^8.28.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"
|
||||
"prettier": "^2.8.0",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.2",
|
||||
|
||||
3632
pnpm-lock.yaml
generated
3632
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.25.0",
|
||||
"typescript": "^4.8.4"
|
||||
"eslint": "^8.28.0",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ export type PageConfig = {
|
||||
|
||||
export type ThemeConfig = {
|
||||
text: string;
|
||||
background: string;
|
||||
primary: string;
|
||||
background: string;
|
||||
};
|
||||
|
||||
export type TypeCategory = 'heading' | 'body';
|
||||
|
||||
@ -125,7 +125,22 @@ export type ListItem =
|
||||
| WorkExperience
|
||||
| Custom;
|
||||
|
||||
export type SectionType = 'basic' | 'custom';
|
||||
export type SectionType =
|
||||
| 'basic'
|
||||
| 'location'
|
||||
| 'profiles'
|
||||
| 'education'
|
||||
| 'awards'
|
||||
| 'certifications'
|
||||
| 'publications'
|
||||
| 'skills'
|
||||
| 'languages'
|
||||
| 'interests'
|
||||
| 'volunteer'
|
||||
| 'projects'
|
||||
| 'references'
|
||||
| 'custom'
|
||||
| 'work';
|
||||
|
||||
export type SectionPath = `sections.${string}`;
|
||||
|
||||
@ -136,4 +151,5 @@ export type Section = {
|
||||
columns: number;
|
||||
visible: boolean;
|
||||
items: ListItem[];
|
||||
isDuplicated?: boolean;
|
||||
};
|
||||
|
||||
@ -8,29 +8,29 @@
|
||||
"start": "node dist/main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.188.0",
|
||||
"@nestjs/axios": "^0.1.0",
|
||||
"@nestjs/common": "^9.1.4",
|
||||
"@aws-sdk/client-s3": "^3.216.0",
|
||||
"@nestjs/axios": "^1.0.0",
|
||||
"@nestjs/common": "^9.2.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.1.4",
|
||||
"@nestjs/core": "^9.2.0",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/mapped-types": "^1.2.0",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.1.4",
|
||||
"@nestjs/platform-express": "^9.2.0",
|
||||
"@nestjs/schedule": "^2.1.0",
|
||||
"@nestjs/serve-static": "^3.0.0",
|
||||
"@nestjs/terminus": "^9.1.2",
|
||||
"@nestjs/terminus": "^9.1.3",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@types/passport": "^1.0.11",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager": "^5.0.1",
|
||||
"cache-manager": "^5.1.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csvtojson": "^2.0.10",
|
||||
"dayjs": "^1.11.5",
|
||||
"google-auth-library": "^8.5.2",
|
||||
"joi": "^17.6.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"google-auth-library": "^8.7.0",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"nanoid": "^3.3.4",
|
||||
@ -41,7 +41,7 @@
|
||||
"passport-local": "^1.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.8.0",
|
||||
"playwright-chromium": "^1.27.1",
|
||||
"playwright-chromium": "^1.28.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.7",
|
||||
@ -49,24 +49,24 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.4",
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/lodash": "^4.14.190",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.0",
|
||||
"@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",
|
||||
"prettier": "^2.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.8.4",
|
||||
"webpack": "^5.74.0"
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.75.0"
|
||||
}
|
||||
}
|
||||
|
||||
5
server/src/config/cache.config.ts
Normal file
5
server/src/config/cache.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('cache', () => ({
|
||||
pdfDeletionTime: parseInt(process.env.PDF_DELETION_TIME, 10) || 4 * 24 * 60 * 60 * 1000, // 4 days
|
||||
}));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user