mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b24da90ba7 | |||
| 2aa7dbd3ad | |||
| 9f8f2c4b8b | |||
| 5331ecccc1 | |||
| f2ec86940c | |||
| cd74e707ba | |||
| ff101dbfac | |||
| 5024c19f87 | |||
| c9850b5815 | |||
| 6fe4e7d7e1 | |||
| a5b8b91e82 | |||
| cc7095adc3 | |||
| e2703f55aa | |||
| 8c5849c988 | |||
| 5322ab2420 | |||
| b84e6bcfb1 | |||
| a4ab0174c7 | |||
| 7e93b5a757 | |||
| 044820fa71 | |||
| 322178e8a4 | |||
| 6358fbad30 | |||
| d342c0a9af | |||
| 63084eebb4 | |||
| 3b4ea00db8 | |||
| c8f7bffe7e | |||
| 3ff56f89d9 | |||
| 7fb9f27837 | |||
| c9685d4ce7 | |||
| 4dc987e27d | |||
| f7af06ae9a | |||
| a5c337faa3 | |||
| fc4704f0a6 | |||
| d968334ada | |||
| fea6d23178 | |||
| 3fefc95572 | |||
| b07e7d1213 | |||
| d47b8bfb03 | |||
| 5bf7fbdae1 | |||
| fca766b382 | |||
| feadfb1b67 | |||
| e69000f221 | |||
| 6b4a54465a | |||
| 878659999f | |||
| 1868c47e30 | |||
| 51442efc23 | |||
| 556e962ec5 | |||
| b5ce67f863 | |||
| c3ce89dc3a |
2
.github/workflows/digitalocean-deploy.yml
vendored
2
.github/workflows/digitalocean-deploy.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Install DigitalOcean CLI
|
||||
uses: digitalocean/action-doctl@v2.2.0
|
||||
uses: digitalocean/action-doctl@v2.3.0
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||
|
||||
|
||||
4
.github/workflows/docker-build-push.yml
vendored
4
.github/workflows/docker-build-push.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.1.0
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- id: version
|
||||
name: App Version
|
||||
@ -45,7 +45,7 @@ jobs:
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
uses: docker/build-push-action@v3.3.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE)
|
||||
[](https://translate.rxresu.me)
|
||||
[](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||

|
||||

|
||||
[](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
||||
|
||||
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
||||
@ -133,6 +134,10 @@ Reactive Resume would be nothing without the folks who supported me and kept the
|
||||
### [GitHub Sponsor](https://github.com/sponsors/AmruthPillai)
|
||||
### [PayPal](https://paypal.me/RajaRajanA)
|
||||
|
||||
## GitHub Star History
|
||||
|
||||
[](https://star-history.com/#AmruthPillai/Reactive-Resume&Date)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- [Next.js](https://nextjs.org/), frontend
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '7.1.2' apply false
|
||||
id 'com.android.library' version '7.1.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.7.22' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
@ -53,13 +53,12 @@ const Header = () => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -108,7 +108,7 @@ const LeftSidebar = () => {
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<nav className="overflow-y-scroll">
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Link href="/dashboard">
|
||||
<Logo size={40} />
|
||||
|
||||
@ -43,7 +43,7 @@ const RightSidebar = () => {
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<nav className="overflow-y-scroll">
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Avatar size={40} />
|
||||
<Divider />
|
||||
|
||||
@ -3,7 +3,7 @@ import { Button } from '@mui/material';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL } from '@/constants/index';
|
||||
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL, REDDIT_URL } from '@/constants/index';
|
||||
|
||||
import styles from './Links.module.scss';
|
||||
|
||||
@ -50,6 +50,12 @@ const Links = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.reddit')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DOCS_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.docs')}
|
||||
|
||||
@ -51,7 +51,7 @@ const Settings = () => {
|
||||
const pageConfig: PageConfig | undefined = useMemo(() => get(resume, 'metadata.page'), [resume]);
|
||||
|
||||
const isDarkMode = useMemo(() => theme === 'dark', [theme]);
|
||||
const exampleDateString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const exampleDateString = useMemo(() => `Eg. ${dayjs().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const themeString = useMemo(() => (isDarkMode ? 'Matte Black Everything' : 'As bright as your future'), [isDarkMode]);
|
||||
|
||||
const { mutateAsync: loadSampleDataMutation } = useMutation<Resume, ServerError, LoadSampleDataParams>(
|
||||
|
||||
@ -6,15 +6,17 @@ import { useState } from 'react';
|
||||
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import getGravatarUrl from '@/utils/getGravatarUrl';
|
||||
|
||||
import styles from './Avatar.module.scss';
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
interactive?: boolean;
|
||||
};
|
||||
|
||||
const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
const Avatar: React.FC<Props> = ({ size = 64, interactive = true }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@ -34,6 +36,11 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleOpenProfile = () => {
|
||||
dispatch(setModalState({ modal: 'auth.profile', state: { open: true } }));
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout());
|
||||
handleClose();
|
||||
@ -43,7 +50,7 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<IconButton onClick={handleOpen} disabled={!interactive}>
|
||||
<Image
|
||||
width={size}
|
||||
height={size}
|
||||
@ -54,9 +61,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
</IconButton>
|
||||
|
||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||
<MenuItem>
|
||||
<MenuItem onClick={handleOpenProfile}>
|
||||
<div>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')},</span>
|
||||
<p>{user?.name}</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
import { isEmpty } from 'lodash';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
type Props = {
|
||||
children?: string;
|
||||
@ -11,7 +14,11 @@ const Markdown: React.FC<Props> = ({ className, children }) => {
|
||||
if (!children || isEmpty(children)) return null;
|
||||
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={[]} className={clsx('markdown', className)}>
|
||||
<ReactMarkdown
|
||||
className={clsx('markdown', className)}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
@ -13,6 +13,11 @@ export const DOCS_URL = 'https://docs.rxresu.me';
|
||||
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
|
||||
export const TRANSLATE_URL = 'https://translate.rxresu.me/';
|
||||
export const DIGITALOCEAN_URL = 'https://pillai.xyz/digitalocean';
|
||||
export const REDDIT_URL = 'https://www.reddit.com/r/reactiveresume/';
|
||||
export const GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
|
||||
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
|
||||
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';
|
||||
|
||||
// Default Error Message
|
||||
export const DEFAULT_ERROR_MESSAGE =
|
||||
'Something went wrong while performing this action, please report this issue on GitHub.';
|
||||
|
||||
@ -62,14 +62,7 @@ const LoginModal: React.FC = () => {
|
||||
};
|
||||
|
||||
const onSubmit = async ({ identifier, password }: FormData) => {
|
||||
await loginMutation(
|
||||
{ identifier, password },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
await loginMutation({ identifier, password });
|
||||
|
||||
handleClose();
|
||||
};
|
||||
@ -86,14 +79,14 @@ const LoginModal: React.FC = () => {
|
||||
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
const handleGoogleLoginError = () => {
|
||||
toast.error("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
const PasswordVisibility = (): React.ReactElement => {
|
||||
@ -117,7 +110,7 @@ const LoginModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
@ -81,14 +81,14 @@ const RegisterModal: React.FC = () => {
|
||||
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
const handleGoogleLoginError = () => {
|
||||
toast("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -100,7 +100,7 @@ const RegisterModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
159
client/modals/auth/UserProfileModal.tsx
Normal file
159
client/modals/auth/UserProfileModal.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { CrisisAlert, ManageAccounts } from '@mui/icons-material';
|
||||
import { Button, Divider, TextField } from '@mui/material';
|
||||
import Joi from 'joi';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import Avatar from '@/components/shared/Avatar';
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
import { deleteAccount, updateProfile, UpdateProfileParams } from '@/services/auth';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const defaultState: FormData = {
|
||||
name: '',
|
||||
email: '',
|
||||
};
|
||||
|
||||
const schema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
email: Joi.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required(),
|
||||
});
|
||||
|
||||
const UserProfileModal = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [deleteText, setDeleteText] = useState<string>('');
|
||||
const isDeleteTextValid = useMemo(() => deleteText.toLowerCase() === 'delete', [deleteText]);
|
||||
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
const { open: isOpen } = useAppSelector((state) => state.modal['auth.profile']);
|
||||
|
||||
const { mutateAsync: deleteAccountMutation } = useMutation<void, ServerError>(deleteAccount);
|
||||
const { mutateAsync: updateProfileMutation } = useMutation<void, ServerError, UpdateProfileParams>(updateProfile);
|
||||
|
||||
const { reset, getFieldState, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
resolver: joiResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user && !getFieldState('name').isTouched && !getFieldState('email').isTouched) {
|
||||
reset({ name: user.name, email: user.email });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setModalState({ modal: 'auth.profile', state: { open: false } }));
|
||||
};
|
||||
|
||||
const handleUpdate = handleSubmit(async (data) => {
|
||||
handleClose();
|
||||
await updateProfileMutation({ name: data.name });
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteAccountMutation();
|
||||
handleClose();
|
||||
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal isOpen={isOpen} handleClose={handleClose} heading="Your Account" icon={<ManageAccounts />}>
|
||||
<div className="grid gap-4">
|
||||
<form className="grid gap-4 xl:w-2/3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar interactive={false} />
|
||||
|
||||
<div className="grid flex-1 gap-1.5">
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.auth.profile.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="pl-4 text-[10.25px] opacity-50">
|
||||
<Trans t={t} i18nKey="modals.auth.profile.form.avatar.help-text">
|
||||
You can update your profile picture on{' '}
|
||||
<a href="https://gravatar.com/" target="_blank" rel="noreferrer">
|
||||
Gravatar
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
disabled
|
||||
label={t('modals.auth.profile.form.email.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={t('modals.auth.profile.form.email.help-text')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button onClick={handleUpdate}>{t('modals.auth.profile.actions.save')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="my-2">
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<CrisisAlert />
|
||||
<h5 className="font-medium">{t('modals.auth.profile.delete-account.heading')}</h5>
|
||||
</div>
|
||||
|
||||
<p className="text-xs opacity-75">{t('modals.auth.profile.delete-account.body', { keyword: 'delete' })}</p>
|
||||
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<TextField
|
||||
value={deleteText}
|
||||
placeholder="Type 'delete' to confirm"
|
||||
onChange={(e) => setDeleteText(e.target.value)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button variant="contained" color="error" disabled={!isDeleteTextValid} onClick={handleDelete}>
|
||||
{t('modals.auth.profile.delete-account.actions.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileModal;
|
||||
@ -134,7 +134,7 @@ const AwardModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -134,7 +134,7 @@ const CertificateModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -151,7 +151,7 @@ const CustomModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -175,7 +175,7 @@ const CustomModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -173,7 +173,7 @@ const EducationModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -197,7 +197,7 @@ const EducationModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -143,7 +143,7 @@ const ProjectModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -167,7 +167,7 @@ const ProjectModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -134,7 +134,7 @@ const PublicationModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -140,7 +140,7 @@ const VolunteerModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -164,7 +164,7 @@ const VolunteerModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -140,7 +140,7 @@ const WorkModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -164,7 +164,7 @@ const WorkModal: React.FC = () => {
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
|
||||
@ -6,7 +6,6 @@ import Joi from 'joi';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
@ -66,15 +65,10 @@ const CreateResumeModal: React.FC = () => {
|
||||
}, [name, setValue]);
|
||||
|
||||
const onSubmit = async ({ name, slug, isPublic }: FormData) => {
|
||||
try {
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
@ -68,8 +68,8 @@ const ImportExternalModal: React.FC = () => {
|
||||
}
|
||||
|
||||
await mutateAsync({ integration, file });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import ForgotPasswordModal from './auth/ForgotPasswordModal';
|
||||
import LoginModal from './auth/LoginModal';
|
||||
import RegisterModal from './auth/RegisterModal';
|
||||
import ResetPasswordModal from './auth/ResetPasswordModal';
|
||||
import UserProfileModal from './auth/UserProfileModal';
|
||||
import AwardModal from './builder/sections/AwardModal';
|
||||
import CertificateModal from './builder/sections/CertificateModal';
|
||||
import CustomModal from './builder/sections/CustomModal';
|
||||
@ -49,6 +50,7 @@ const ModalWrapper: React.FC = () => {
|
||||
<RegisterModal />
|
||||
<ForgotPasswordModal />
|
||||
<ResetPasswordModal />
|
||||
<UserProfileModal />
|
||||
|
||||
{/* Dashboard */}
|
||||
<CreateResumeModal />
|
||||
|
||||
@ -17,14 +17,13 @@
|
||||
"@hookform/resolvers": "2.9.10",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/lab": "^5.0.0-alpha.112",
|
||||
"@mui/material": "^5.11.0",
|
||||
"@mui/system": "^5.11.0",
|
||||
"@mui/x-date-pickers": "5.0.11",
|
||||
"@next/env": "^13.0.7",
|
||||
"@react-oauth/google": "^0.5.1",
|
||||
"@mui/lab": "^5.0.0-alpha.116",
|
||||
"@mui/material": "^5.11.5",
|
||||
"@mui/system": "^5.11.5",
|
||||
"@mui/x-date-pickers": "5.0.14",
|
||||
"@react-oauth/google": "^0.6.0",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"axios": "^1.2.1",
|
||||
"axios": "^1.2.3",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"downloadjs": "^1.4.7",
|
||||
@ -32,51 +31,52 @@
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "13.0.7",
|
||||
"next-i18next": "^13.0.2",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "13.1.2",
|
||||
"next-i18next": "^13.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.40.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-query": "^3.39.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"react-zoom-pan-pinch": "^2.2.0",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.2.2",
|
||||
"redux-undo": "^1.0.1",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.2",
|
||||
"remark-math": "^5.1.1",
|
||||
"sharp": "^0.31.3",
|
||||
"uuid": "^9.0.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.5",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.15",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/webfontloader": "^1.6.35",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint-config-next": "^13.0.7",
|
||||
"eslint-plugin-tailwindcss": "^3.7.1",
|
||||
"eslint-config-next": "^13.1.2",
|
||||
"eslint-plugin-tailwindcss": "^3.8.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"next-sitemap": "^3.1.42",
|
||||
"postcss": "^8.4.20",
|
||||
"sass": "^1.56.2",
|
||||
"next-sitemap": "^3.1.45",
|
||||
"postcss": "^8.4.21",
|
||||
"sass": "^1.57.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { DEFAULT_ERROR_MESSAGE } from '@/constants/index';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
@ -105,7 +106,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
toast.error(DEFAULT_ERROR_MESSAGE);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL, REDDIT_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
@ -177,6 +177,12 @@ const Home: NextPage = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.reddit')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.donate')}
|
||||
|
||||
@ -11,7 +11,6 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
@ -69,17 +68,13 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({
|
||||
username: resume.user.username,
|
||||
slug: resume.slug,
|
||||
lastUpdated: dayjs(resume.updatedAt).unix().toString(),
|
||||
});
|
||||
const url = await mutateAsync({
|
||||
username: resume.user.username,
|
||||
slug: resume.slug,
|
||||
lastUpdated: dayjs(resume.updatedAt).unix().toString(),
|
||||
});
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
}
|
||||
download(url);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"present": "Heute"
|
||||
},
|
||||
"subtitle": "Ein kostenloser Open Source Lebenslauf-Baukasten.",
|
||||
"title": "Reactive Resume",
|
||||
"title": "Reaktives Lebenslauf",
|
||||
"toast": {
|
||||
"error": {
|
||||
"upload-file-size": "Bitte laden Sie nur Dateien unter 2 Megabytes hoch.",
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
"actions": {
|
||||
"add": "Add New {{token}}",
|
||||
"delete": "Delete {{token}}",
|
||||
"edit": "Edit {{token}}",
|
||||
"duplicate": "Duplicate Section"
|
||||
"duplicate": "Duplicate Section",
|
||||
"edit": "Edit {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
"heading": "Columns",
|
||||
@ -81,13 +81,13 @@
|
||||
"center-artboard": "Center Artboard",
|
||||
"copy-link": "Copy Link to Resume",
|
||||
"export-pdf": "Export PDF",
|
||||
"redo": "Redo",
|
||||
"toggle-orientation": "Toggle Page Orientation",
|
||||
"toggle-page-break-line": "Toggle Page Break Line",
|
||||
"toggle-sidebars": "Toggle Sidebars",
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -115,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Photo Filters"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Date of Birth"
|
||||
},
|
||||
"heading": "Basics",
|
||||
"headline": {
|
||||
"label": "Headline"
|
||||
@ -122,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Full Name"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Date of Birth"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -265,14 +265,15 @@
|
||||
"button": "GitHub Issues",
|
||||
"heading": "Bugs? Feature Requests?"
|
||||
},
|
||||
"docs": "Documentation",
|
||||
"donate": {
|
||||
"body": "If you liked using Reactive Resume, please consider donating as much as you can to the cause of keeping the app up and running, without ads and free forever.",
|
||||
"button": "Buy me a coffee",
|
||||
"heading": "Donate to Reactive Resume"
|
||||
},
|
||||
"github": "Source Code",
|
||||
"docs": "Documentation",
|
||||
"heading": "Links"
|
||||
"heading": "Links",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -291,14 +292,14 @@
|
||||
},
|
||||
"heading": "Settings",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "Paper Size",
|
||||
"secondary": "Determines the dimensions of your resume pages"
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "Break Line",
|
||||
"secondary": "Show a line on all pages to mark the height of an A4 page"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Paper Size",
|
||||
"secondary": "Determines the dimensions of your resume pages"
|
||||
},
|
||||
"heading": "Page",
|
||||
"orientation": {
|
||||
"disabled": "Has no effect when there is only one page",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Links",
|
||||
"links": {
|
||||
"docs": "Documentation",
|
||||
"donate": "Donate",
|
||||
"github": "Source Code",
|
||||
"docs": "Documentation",
|
||||
"privacy": "Privacy Policy",
|
||||
"reddit": "Reddit",
|
||||
"service": "Terms of Service"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Screenshots"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Testimonials",
|
||||
"body": "Good or bad, I would love to hear your opinion on Reactive Resume and how the experience has been for you.<br/>Here are some of the messages sent in by users across the world.",
|
||||
"contact": "You can reach out to me through <1>my email</1> or through the contact form on <3>my website</3>."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3. With this app, you can create multiple resumes, share them with recruiters or friends through a unique link and print it as a PDF, all for free, no ads, no tracking, without losing the integrity and privacy of your data.",
|
||||
"heading": "Summary"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Good or bad, I would love to hear your opinion on Reactive Resume and how the experience has been for you.<br/>Here are some of the messages sent in by users across the world.",
|
||||
"contact": "You can reach out to me through <1>my email</1> or through the contact form on <3>my website</3>.",
|
||||
"heading": "Testimonials"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "Reset your password"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "Your Account",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "You can update your profile picture on <1>Gravatar</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "Full Name"
|
||||
},
|
||||
"email": {
|
||||
"label": "Email Address",
|
||||
"help-text": "It is not possible to update your email address at the moment, please create a new account instead."
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "Delete Account and Data",
|
||||
"body": "To delete your account, your data and all your resumes, type \"{{keyword}}\" into the textbox and click on the button. Please note that this is an irreversible action and your data cannot be retrieved again.",
|
||||
"actions": {
|
||||
"delete": "Delete Account"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save Changes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "Agregar nuevo {{token}}",
|
||||
"delete": "Eliminar {{token}}",
|
||||
"duplicate": "Sección duplicada",
|
||||
"edit": "Editar {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "Centrar Tablero",
|
||||
"copy-link": "Copiar enlace al currículum",
|
||||
"export-pdf": "Exportar PDF",
|
||||
"redo": "Rehacer",
|
||||
"toggle-orientation": "Cambiar la orientación de la página",
|
||||
"toggle-page-break-line": "Alternar línea de salto de página",
|
||||
"toggle-sidebars": "Ocultar/mostrar barra lateral",
|
||||
"zoom-in": "Acercar",
|
||||
"zoom-out": "Alejar",
|
||||
"undo": "Deshacer",
|
||||
"redo": "Rehacer"
|
||||
"zoom-in": "Acercar",
|
||||
"zoom-out": "Alejar"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Filtro de fotos"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Fecha de cumpleaños"
|
||||
},
|
||||
"heading": "Información básica",
|
||||
"headline": {
|
||||
"label": "Titular"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Nombre Completo"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Fecha de cumpleaños"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -264,14 +265,15 @@
|
||||
"button": "Propuesta de GitHub",
|
||||
"heading": "¿Errores? ¿Solicitud de características?"
|
||||
},
|
||||
"docs": "Documentación",
|
||||
"donate": {
|
||||
"body": "Si le gustó usar Reactive Resume, considere donar lo que pueda a la causa de mantener la aplicación en funcionamiento, sin anuncios y gratis para siempre.",
|
||||
"button": "Invítame a un café",
|
||||
"heading": "Donar a Reactive Resume"
|
||||
},
|
||||
"github": "Código Fuente",
|
||||
"docs": "Documentación",
|
||||
"heading": "Enlaces"
|
||||
"heading": "Enlaces",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -290,14 +292,14 @@
|
||||
},
|
||||
"heading": "Preferencias",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "Tamaño de papel",
|
||||
"secondary": "Determina las dimensiones de las páginas de tu currículum."
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "Linea de separación",
|
||||
"secondary": "Mostrar una línea en todas las páginas para marcar la altura de una página A4"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Tamaño de papel",
|
||||
"secondary": "Determina las dimensiones de las páginas de tu currículum."
|
||||
},
|
||||
"heading": "Página",
|
||||
"orientation": {
|
||||
"disabled": "No tiene efecto cuando solo hay una página",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Enlaces",
|
||||
"links": {
|
||||
"docs": "Documentación",
|
||||
"donate": "Donar",
|
||||
"github": "Código fuente",
|
||||
"docs": "Documentación",
|
||||
"privacy": "Política de Privacidad",
|
||||
"reddit": "Reddit",
|
||||
"service": "Términos de Servicio"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Capturas de pantalla"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Opiniones",
|
||||
"body": "Bueno o malo, me encantaría saber tu opinión sobre Reactive Resume y cómo ha sido la experiencia para ti.<br/>Estos son algunos de los mensajes enviados por usuarios de todo el mundo.",
|
||||
"contact": "Puedes comunicarte conmigo a través de <1>mi correo electrónico</1> o a través del formulario de contacto en <3>mi sitio web</3>."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume es un generador de currículums gratuito y de código abierto que está diseñado para hacer que las tareas mundanas de crear, actualizar y compartir su currículum sean tan fáciles como 1, 2, 3. Con esta aplicación, puede crear múltiples currículums, compartirlos con reclutadores o amigos a través de un enlace único e imprímalo como PDF, todo gratis, sin anuncios, sin seguimiento, sin perder la integridad y privacidad de sus datos.",
|
||||
"heading": "Resumen"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Bueno o malo, me encantaría saber tu opinión sobre Reactive Resume y cómo ha sido la experiencia para ti.<br/>Estos son algunos de los mensajes enviados por usuarios de todo el mundo.",
|
||||
"contact": "Puedes comunicarte conmigo a través de <1>mi correo electrónico</1> o a través del formulario de contacto en <3>mi sitio web</3>.",
|
||||
"heading": "Opiniones"
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "{{token}} جدید اضافه کنید",
|
||||
"delete": "حذف {{token}}",
|
||||
"duplicate": "بخش تکراری",
|
||||
"edit": "ویرایش {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "قرار دادن صفحه در مرکز",
|
||||
"copy-link": "کپی کردن لینک رزومه",
|
||||
"export-pdf": "خروجی PDF",
|
||||
"redo": "دوباره انجام دهید",
|
||||
"toggle-orientation": "تغییر وضعیت جهتگیری صفحه",
|
||||
"toggle-page-break-line": "تغییر وضعیت خط شکست صفحه",
|
||||
"toggle-sidebars": "باز/بسته کردن نوار کنار صفحه",
|
||||
"zoom-in": "بزرگنمایی",
|
||||
"zoom-out": "کوچکنمایی",
|
||||
"undo": "واگرد",
|
||||
"redo": "دوباره انجام دهید"
|
||||
"zoom-in": "بزرگنمایی",
|
||||
"zoom-out": "کوچکنمایی"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "فیلترهای تصویر"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "تاریخ تولد"
|
||||
},
|
||||
"heading": "موارد پایه",
|
||||
"headline": {
|
||||
"label": "سرصفحه"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "نام کامل"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "تاریخ تولد"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -264,14 +265,15 @@
|
||||
"button": "GitHub Issues",
|
||||
"heading": "باگها؟ درخواست ویژگی جدید؟"
|
||||
},
|
||||
"docs": "مستندات",
|
||||
"donate": {
|
||||
"body": "اگر استفاده از Reactive Resume را دوست داشتید، لطفاً تا جایی که می توانید کمک مالی کنید تا برنامه را بدون تبلیغات و برای همیشه رایگان نگه دارید.",
|
||||
"button": "برای من یک قهوه بخر",
|
||||
"heading": "کمک مالی به Reactive Resume"
|
||||
},
|
||||
"github": "کد منبع",
|
||||
"docs": "مستندات",
|
||||
"heading": "لینکها"
|
||||
"heading": "لینکها",
|
||||
"reddit": "ردیت"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -290,14 +292,14 @@
|
||||
},
|
||||
"heading": "تنظیمات",
|
||||
"page": {
|
||||
"format": {
|
||||
"primary": "اندازه کاغذ",
|
||||
"secondary": "ابعاد صفحات رزومه شما را تعیین می کند"
|
||||
},
|
||||
"break-line": {
|
||||
"primary": "خط شکست",
|
||||
"secondary": "برای مشخص کردن ارتفاع صفحه A4 یک خط در همه صفحات نشان داده شود"
|
||||
},
|
||||
"format": {
|
||||
"primary": "اندازه کاغذ",
|
||||
"secondary": "ابعاد صفحات رزومه شما را تعیین می کند"
|
||||
},
|
||||
"heading": "صفحه",
|
||||
"orientation": {
|
||||
"disabled": "زمانی که تنها یک صفحه وجود دارد، تاثیری ندارد",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "لینکها",
|
||||
"links": {
|
||||
"docs": "مستندات",
|
||||
"donate": "حمایت مالی",
|
||||
"github": "کد منبع",
|
||||
"docs": "مستندات",
|
||||
"privacy": "حریم خصوصی",
|
||||
"reddit": "ردیت",
|
||||
"service": "شرایط سرویسدهی"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "اسکرینشاتها"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "نظرات کاربران",
|
||||
"body": "خوب یا بد، من دوست دارم نظر شما را در مورد Reactive Resume و اینکه تجربه کار با آن برای شما چگونه بوده است را بدانم.<br/>تعدادی از پیام های ارسال شده توسط کاربران در سراسر جهان را اینجا میبینید.",
|
||||
"contact": "میتوانید از طریق <1>ایمیل من</1> یا فرم تماس در <3>وبسایت من</3> با من در ارتباط باشید."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume یک رزومه ساز رایگان و متنباز است که برای ایجاد، به روز رسانی و به اشتراک گذاری رزومه شما به آسانی شمردن ۱، ۲، ۳ ساخته شده است. با این برنامه، می توانید چندین رزومه ایجاد کنید و آنها را با کارفرماها یا دوستان از طریق یک لینک منحصر به فرد و چاپ آن به صورت PDF، همه به صورت رایگان، بدون تبلیغات، بدون ردیابی، بدون از دست دادن امنیت و حریم خصوصی داده های شما، به اشتراک بگذارید.",
|
||||
"heading": "درباره من"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "خوب یا بد، من دوست دارم نظر شما را در مورد Reactive Resume و اینکه تجربه کار با آن برای شما چگونه بوده است را بدانم.<br/>تعدادی از پیام های ارسال شده توسط کاربران در سراسر جهان را اینجا میبینید.",
|
||||
"contact": "میتوانید از طریق <1>ایمیل من</1> یا فرم تماس در <3>وبسایت من</3> با من در ارتباط باشید.",
|
||||
"heading": "نظرات کاربران"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"avatar": {
|
||||
"menu": {
|
||||
"greeting": "Hallo",
|
||||
"logout": "Afmelden"
|
||||
"logout": "Uitloggen"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
@ -10,7 +10,7 @@
|
||||
"license": "Door de gemeenschap, voor de gemeenschap."
|
||||
},
|
||||
"markdown": {
|
||||
"help-text": "Deze sectie ondersteunt <1>markdown</1> opmaak."
|
||||
"help-text": "Deze sectie ondersteunt <1>html</1> opmaak."
|
||||
},
|
||||
"date": {
|
||||
"present": "Heden"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /*/*
|
||||
|
||||
# Host
|
||||
Host: https://rxresu.me
|
||||
|
||||
@ -2,7 +2,7 @@ import { User } from '@reactive-resume/schema';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { setAccessToken, setUser } from '@/store/auth/authSlice';
|
||||
import { logout, setAccessToken, setUser } from '@/store/auth/authSlice';
|
||||
|
||||
import store from '../store';
|
||||
import axios from './axios';
|
||||
@ -37,6 +37,10 @@ export type ResetPasswordParams = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type UpdateProfileParams = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const login = async (loginParams: LoginParams) => {
|
||||
const {
|
||||
data: { user, accessToken },
|
||||
@ -75,3 +79,23 @@ export const resetPassword = async (resetPasswordParams: ResetPasswordParams) =>
|
||||
|
||||
toast.success('Your password has been changed successfully, please login again.');
|
||||
};
|
||||
|
||||
export const updateProfile = async (updateProfileParams: UpdateProfileParams) => {
|
||||
const { data: user } = await axios.patch<User, AxiosResponse<User>, UpdateProfileParams>(
|
||||
'/auth/update-profile',
|
||||
updateProfileParams
|
||||
);
|
||||
|
||||
store.dispatch(setUser(user));
|
||||
|
||||
toast.success('Your profile has been successfully updated.');
|
||||
};
|
||||
|
||||
export const deleteAccount = async () => {
|
||||
await axios.delete('/resume/all');
|
||||
await axios.delete('/auth');
|
||||
|
||||
store.dispatch(logout());
|
||||
|
||||
toast.success('Your account has been deleted, hope to see you again soon.');
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import _axios from 'axios';
|
||||
import _axios, { AxiosError } from 'axios';
|
||||
import Router from 'next/router';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
|
||||
@ -19,22 +20,22 @@ const axios = _axios.create({ baseURL });
|
||||
axios.interceptors.request.use((config) => {
|
||||
const { accessToken } = store.getState().auth;
|
||||
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
};
|
||||
config.headers.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
(error: AxiosError<ServerError>) => {
|
||||
const { response } = error;
|
||||
|
||||
if (response) {
|
||||
const errorObject: ServerError = response.data;
|
||||
const errorObject = response.data;
|
||||
const code = errorObject.statusCode;
|
||||
const message = errorObject.message;
|
||||
|
||||
toast.error(message);
|
||||
|
||||
if (code === 401 || code === 404) {
|
||||
store.dispatch(logout());
|
||||
|
||||
@ -39,7 +39,7 @@ export const buildSlice = createSlice({
|
||||
name: 'build',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTheme: (state, action: PayloadAction<SetThemePayload>) => {
|
||||
setTheme: (state: BuildState, action: PayloadAction<SetThemePayload>) => {
|
||||
const { theme } = action.payload;
|
||||
|
||||
state.theme = theme;
|
||||
|
||||
@ -5,6 +5,7 @@ export type ModalName =
|
||||
| 'auth.register'
|
||||
| 'auth.forgot'
|
||||
| 'auth.reset'
|
||||
| 'auth.profile'
|
||||
| 'dashboard.create-resume'
|
||||
| 'dashboard.import-external'
|
||||
| 'dashboard.rename-resume'
|
||||
@ -24,6 +25,7 @@ const initialState: Record<ModalName, ModalState> = {
|
||||
'auth.register': { open: false },
|
||||
'auth.forgot': { open: false },
|
||||
'auth.reset': { open: false },
|
||||
'auth.profile': { open: false },
|
||||
'dashboard.create-resume': { open: false },
|
||||
'dashboard.import-external': { open: false },
|
||||
'dashboard.rename-resume': { open: false },
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
// KaTeX (for remark-math)
|
||||
@import url('https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css');
|
||||
|
||||
// Tailwind CSS
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@ -35,6 +38,10 @@
|
||||
ul li {
|
||||
@apply ml-4 list-outside;
|
||||
}
|
||||
|
||||
.footnotes p {
|
||||
@apply inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ const Section: React.FC<SectionProps> = ({
|
||||
{Array.from(Array(8).keys()).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mr-1 h-2 w-4 rounded-sm border"
|
||||
className="mr-1 h-2 w-full max-w-[1rem] rounded-sm border"
|
||||
style={{
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: levelNum / (10 / 8) > index ? primaryColor : '',
|
||||
|
||||
@ -10,7 +10,7 @@ export const dateFormatOptions: string[] = [
|
||||
'DD.MM.YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'MM.DD.YYYY',
|
||||
'M.D.YYYY',
|
||||
'M.D.YYYY',
|
||||
'MM/DD/YYYY',
|
||||
'YYYY.MM.DD',
|
||||
'YYYY/MM/DD',
|
||||
@ -20,7 +20,7 @@ export const dateFormatOptions: string[] = [
|
||||
'YYYY',
|
||||
];
|
||||
|
||||
export const getRelativeTime = (timestamp: dayjs.ConfigType): string => dayjs(timestamp).utc().toNow(true);
|
||||
export const getRelativeTime = (timestamp: dayjs.ConfigType): string => dayjs(timestamp).toNow(true);
|
||||
|
||||
export const formatDateString = (date: string | DateRange, formatStr: string): string | null => {
|
||||
const presentString = i18n?.t<string>('common.date.present') ?? '';
|
||||
@ -31,7 +31,7 @@ export const formatDateString = (date: string | DateRange, formatStr: string): s
|
||||
if (isString(date)) {
|
||||
if (!dayjs(date).isValid()) return null;
|
||||
|
||||
return dayjs(date).format(formatStr);
|
||||
return dayjs.utc(date).local().format(formatStr);
|
||||
}
|
||||
|
||||
// If `date` is a DateRange
|
||||
@ -40,12 +40,12 @@ export const formatDateString = (date: string | DateRange, formatStr: string): s
|
||||
if (!dayjs(date.start).isValid()) return null;
|
||||
|
||||
if (dayjs(date.start).isSame(date.end)) {
|
||||
return dayjs(date.start).format(formatStr);
|
||||
return dayjs.utc(date.start).local().format(formatStr);
|
||||
}
|
||||
|
||||
if (!isEmpty(date.end) && dayjs(date.end).isValid()) {
|
||||
return `${dayjs(date.start).format(formatStr)} - ${dayjs(date.end).format(formatStr)}`;
|
||||
return `${dayjs.utc(date.start).local().format(formatStr)} - ${dayjs.utc(date.end).local().format(formatStr)}`;
|
||||
}
|
||||
|
||||
return `${dayjs(date.start).format(formatStr)} - ${presentString}`;
|
||||
return `${dayjs.utc(date.start).local().format(formatStr)} - ${presentString}`;
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
FaHackerrank,
|
||||
FaInstagram,
|
||||
FaLinkedinIn,
|
||||
FaMastodon,
|
||||
FaMedium,
|
||||
FaSkype,
|
||||
FaSoundcloud,
|
||||
@ -18,10 +19,11 @@ import {
|
||||
FaXing,
|
||||
FaYoutube,
|
||||
} from 'react-icons/fa';
|
||||
import { SiCodechef, SiCodeforces } from 'react-icons/si';
|
||||
import { SiCodeberg, SiCodechef, SiCodeforces } from 'react-icons/si';
|
||||
|
||||
const profileIconMap: Record<string, JSX.Element> = {
|
||||
behance: <FaBehance />,
|
||||
codeberg: <SiCodeberg />,
|
||||
codechef: <SiCodechef />,
|
||||
codeforces: <SiCodeforces />,
|
||||
dribbble: <FaDribbble />,
|
||||
@ -31,6 +33,7 @@ const profileIconMap: Record<string, JSX.Element> = {
|
||||
hackerrank: <FaHackerrank />,
|
||||
instagram: <FaInstagram />,
|
||||
linkedin: <FaLinkedinIn />,
|
||||
mastodon: <FaMastodon />,
|
||||
medium: <FaMedium />,
|
||||
skype: <FaSkype />,
|
||||
soundcloud: <FaSoundcloud />,
|
||||
|
||||
@ -20,7 +20,7 @@ export const formatLocation = (location?: Location): string => {
|
||||
};
|
||||
|
||||
export const addHttp = (url: string) => {
|
||||
if (url.search(/^http[s]?:\/\//) == -1) {
|
||||
if (url.search(/^http[s]?:\/\//) == -1 && url.search(/^mailto:/) == -1 && url.search(/^tel:/) == -1) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
|
||||
@ -13,9 +13,6 @@ const DateWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) =
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
// Set Default Timezone to UTC
|
||||
dayjs.tz.setDefault('UTC');
|
||||
|
||||
// Locales
|
||||
require('dayjs/locale/am');
|
||||
require('dayjs/locale/ar');
|
||||
|
||||
20
package.json
20
package.json
@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "reactive-resume",
|
||||
"version": "3.6.16",
|
||||
"version": "3.6.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent cross-var cross-env VERSION=$npm_package_version turbo run dev",
|
||||
"build": "env-cmd --silent cross-var cross-env VERSION=$npm_package_version turbo run build",
|
||||
"start": "env-cmd --silent cross-var cross-env VERSION=$npm_package_version turbo run start",
|
||||
"update-deps": "ncu -x nanoid,class-validator --deep -u && pnpm install",
|
||||
"generate-env": "ts-node ./scripts/generate-env.ts",
|
||||
"format": "prettier --write .",
|
||||
"lint": "turbo run lint"
|
||||
@ -19,17 +20,18 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-var": "^1.1.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"turbo": "^1.6.3",
|
||||
"turbo": "^1.7.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.15",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"prettier": "^2.8.1",
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-simple-import-sort": "^9.0.0",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"prettier": "^2.8.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
|
||||
3309
pnpm-lock.yaml
generated
3309
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.29.0",
|
||||
"eslint": "^8.32.0",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ COPY --from=dependencies /app/server/node_modules ./server/node_modules
|
||||
|
||||
RUN pnpm run build --filter server
|
||||
|
||||
FROM mcr.microsoft.com/playwright:v1.28.1-focal as production
|
||||
FROM mcr.microsoft.com/playwright:v1.29.2-focal as production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
"start": "node dist/main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.231.0",
|
||||
"@nestjs/axios": "^1.0.0",
|
||||
"@aws-sdk/client-s3": "^3.252.0",
|
||||
"@nestjs/axios": "^1.0.1",
|
||||
"@nestjs/common": "^9.2.1",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.2.1",
|
||||
"@nestjs/jwt": "^9.0.0",
|
||||
"@nestjs/jwt": "^10.0.1",
|
||||
"@nestjs/mapped-types": "^1.2.0",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.2.1",
|
||||
@ -32,40 +32,41 @@
|
||||
"google-auth-library": "^8.7.0",
|
||||
"joi": "^17.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"nanoid": "^3.3.4",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nanoid": "3.3.4",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"nodemailer": "^6.8.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.8.0",
|
||||
"playwright-chromium": "^1.28.1",
|
||||
"playwright-chromium": "^1.29.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^4.1.1",
|
||||
"rxjs": "^7.8.0",
|
||||
"typeorm": "0.3.11",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@nestjs/cli": "^9.1.8",
|
||||
"@nestjs/schematics": "^9.0.4",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.15",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"prettier": "^2.8.1",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"prettier": "^2.8.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.1",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0"
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Post, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { User } from '@/decorators/user.decorator';
|
||||
import { User as UserEntity } from '@/users/entities/user.entity';
|
||||
@ -7,6 +7,7 @@ import { AuthService } from './auth.service';
|
||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||
import { LocalAuthGuard } from './guards/local.guard';
|
||||
|
||||
@ -57,6 +58,13 @@ export class AuthController {
|
||||
return this.authService.resetPassword(resetPasswordDto);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Patch('update-profile')
|
||||
updateProfile(@User('id') userId: number, @Body() updateProfileDto: UpdateProfileDto) {
|
||||
return this.authService.updateProfile(userId, updateProfileDto);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete()
|
||||
|
||||
@ -6,12 +6,14 @@ import { compareSync, hashSync } from 'bcryptjs';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
|
||||
import { PostgresErrorCode } from '@/database/errorCodes.enum';
|
||||
import { ResumeService } from '@/resume/resume.service';
|
||||
import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto';
|
||||
import { User } from '@/users/entities/user.entity';
|
||||
import { UsersService } from '@/users/users.service';
|
||||
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -68,6 +70,10 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
updateProfile(id: number, newData: UpdateProfileDto) {
|
||||
return this.usersService.update(id, { name: newData.name });
|
||||
}
|
||||
|
||||
forgotPassword(email: string) {
|
||||
return this.usersService.generateResetToken(email);
|
||||
}
|
||||
|
||||
7
server/src/auth/dto/update-profile.dto.ts
Normal file
7
server/src/auth/dto/update-profile.dto.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
}
|
||||
@ -15,7 +15,6 @@ import {
|
||||
} from '@reactive-resume/schema';
|
||||
import csv from 'csvtojson';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { readFile, unlink } from 'fs/promises';
|
||||
import { cloneDeep, get, isEmpty, merge } from 'lodash';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
@ -29,9 +28,7 @@ import { ResumeService } from '@/resume/resume.service';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationsService {
|
||||
constructor(private resumeService: ResumeService) {
|
||||
dayjs.extend(utc);
|
||||
}
|
||||
constructor(private resumeService: ResumeService) {}
|
||||
|
||||
async linkedIn(userId: number, path: string): Promise<ResumeEntity> {
|
||||
let archive: StreamZip.StreamZipAsync;
|
||||
@ -43,7 +40,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(defaultState);
|
||||
|
||||
// Basics
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from LinkedIn (${timestamp})`,
|
||||
slug: `imported-from-linkedin-${timestamp}`,
|
||||
@ -276,7 +273,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(defaultState);
|
||||
|
||||
// Metadata
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from JSON Resume (${timestamp})`,
|
||||
slug: `imported-from-json-resume-${timestamp}`,
|
||||
@ -611,7 +608,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(jsonResume);
|
||||
|
||||
// Metadata
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from Reactive Resume (${timestamp})`,
|
||||
slug: `imported-from-reactive-resume-${timestamp}`,
|
||||
@ -632,7 +629,7 @@ export class IntegrationsService {
|
||||
const resume: Partial<Resume> = cloneDeep(defaultState);
|
||||
|
||||
// Metadata
|
||||
const timestamp = dayjs().utc().format(FILENAME_TIMESTAMP);
|
||||
const timestamp = dayjs().format(FILENAME_TIMESTAMP);
|
||||
merge<Partial<Resume>, DeepPartial<Resume>>(resume, {
|
||||
name: `Imported from Reactive Resume V2 (${timestamp})`,
|
||||
slug: `imported-from-reactive-resume-v2-${timestamp}`,
|
||||
@ -958,6 +955,6 @@ export class IntegrationsService {
|
||||
}
|
||||
|
||||
private parseDate = (date: string): string => {
|
||||
return isEmpty(date) ? '' : dayjs(date).utc().toISOString();
|
||||
return isEmpty(date) ? '' : dayjs(date).toISOString();
|
||||
};
|
||||
}
|
||||
|
||||
@ -31,10 +31,8 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
const publicUrl = `${serverUrl}/assets/exports/${filename}`;
|
||||
|
||||
try {
|
||||
// check if file already exists
|
||||
await access(join(directory, filename));
|
||||
} catch {
|
||||
// delete old files and scheduler jobs
|
||||
const activeSchedulerTimeouts = this.schedulerRegistry.getTimeouts();
|
||||
await readdir(directory).then(async (files) => {
|
||||
await Promise.all(
|
||||
@ -49,7 +47,6 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
});
|
||||
|
||||
// create file as it doesn't exist
|
||||
const url = this.configService.get<string>('app.url');
|
||||
const secretKey = this.configService.get<string>('app.secretKey');
|
||||
const pdfDeletionTime = this.configService.get<number>('cache.pdfDeletionTime');
|
||||
@ -57,8 +54,8 @@ export class PrinterService implements OnModuleInit, OnModuleDestroy {
|
||||
const page = await this.browser.newPage();
|
||||
|
||||
await page.goto(`${url}/${username}/${slug}/printer?secretKey=${secretKey}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('html.wf-active');
|
||||
await page.waitForLoadState("networkidle")
|
||||
|
||||
const pageFormat: PageConfig['format'] = await page.$$eval(
|
||||
'[data-page]',
|
||||
|
||||
@ -5,9 +5,9 @@ const sampleData: Partial<Resume> = {
|
||||
name: 'Alexis Jones',
|
||||
email: 'alexis.jones@gmail.com',
|
||||
phone: '+1 800 1200 3820',
|
||||
birthdate: '1995-08-06T00:00:00.000Z',
|
||||
birthdate: '1995-08-06',
|
||||
photo: {
|
||||
url: `/images/sample-photo.jpg`,
|
||||
url: `https://i.imgur.com/O7iT9ke.jpg`,
|
||||
filters: {
|
||||
size: 128,
|
||||
shape: 'rounded-square',
|
||||
@ -53,7 +53,7 @@ const sampleData: Partial<Resume> = {
|
||||
url: 'https://www.espritcam.com',
|
||||
date: {
|
||||
end: '',
|
||||
start: '2015-09-01T16:34:27.000Z',
|
||||
start: '2015-09-01',
|
||||
},
|
||||
name: 'DP Technology Corp.',
|
||||
summary:
|
||||
@ -64,8 +64,8 @@ const sampleData: Partial<Resume> = {
|
||||
id: '285d78f8-df56-4569-ba6b-cff5ebe5381e',
|
||||
url: 'https://www.vokophone.com',
|
||||
date: {
|
||||
end: '2015-07-31T22:00:00.000Z',
|
||||
start: '2011-05-31T22:00:00.000Z',
|
||||
end: '2015-07-31',
|
||||
start: '2011-05-31',
|
||||
},
|
||||
name: 'Voko Communications',
|
||||
summary:
|
||||
@ -84,7 +84,7 @@ const sampleData: Partial<Resume> = {
|
||||
{
|
||||
title: 'Blitz Hackathon',
|
||||
awarder: '2nd Place',
|
||||
date: '2018-03-31T22:00:00.000Z',
|
||||
date: '2018-03-31',
|
||||
url: '',
|
||||
summary: '',
|
||||
id: '657cadb0-c07d-4a35-8351-9079598c7ac0',
|
||||
@ -92,7 +92,7 @@ const sampleData: Partial<Resume> = {
|
||||
{
|
||||
title: 'Carl-Zeiss Hackathon',
|
||||
awarder: '2nd Place',
|
||||
date: '2017-05-09T22:00:00.000Z',
|
||||
date: '2017-05-09',
|
||||
url: '',
|
||||
summary: '',
|
||||
id: 'db3bc5cb-483e-4221-9867-9c28ee5f2051',
|
||||
|
||||
@ -71,6 +71,12 @@ export class ResumeController {
|
||||
return this.resumeService.update(+id, updateResumeDto, userId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('/all')
|
||||
removeAllByUser(@User('id') userId: number) {
|
||||
return this.resumeService.removeAllByUser(userId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string, @User('id') userId: number) {
|
||||
|
||||
@ -176,8 +176,12 @@ export class ResumeService {
|
||||
return this.resumeRepository.save<Resume>(updatedResume);
|
||||
}
|
||||
|
||||
async remove(id: number, userId: number) {
|
||||
await this.resumeRepository.delete({ id, user: { id: userId } });
|
||||
remove(id: number, userId: number) {
|
||||
return this.resumeRepository.delete({ id, user: { id: userId } });
|
||||
}
|
||||
|
||||
removeAllByUser(userId: number) {
|
||||
return this.resumeRepository.delete({ user: { id: userId } });
|
||||
}
|
||||
|
||||
async duplicate(id: number, userId: number) {
|
||||
|
||||
Reference in New Issue
Block a user