feat(all): upgrade to v3.4.0

This commit is contained in:
Amruth Pillai 2022-04-30 12:58:17 +02:00
parent ccfb4d3cb0
commit 87d381fe8e
61 changed files with 23676 additions and 1291 deletions

View File

@ -1,4 +1,4 @@
ARG VARIANT="16-bullseye" ARG VARIANT="lts-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

View File

@ -2,6 +2,8 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [3.4.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.4.0) (2022-04-30)
### [3.3.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.3.4) (2022-04-09) ### [3.3.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.3.4) (2022-04-09)
### [3.3.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.2...v3.3.3) (2022-04-09) ### [3.3.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.2...v3.3.3) (2022-04-09)

View File

@ -17,16 +17,20 @@ You have complete control over what goes into your resume, how it looks, what co
## Table of Contents ## Table of Contents
- [Features](#features) - [Reactive Resume](#reactive-resume)
- [Languages](#languages) - [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
- [Tutorial](#tutorial) - [Table of Contents](#table-of-contents)
- [Build from Source](#build-from-source) - [Features](#features)
- [Contributing](#contributing) - [Languages](#languages)
- [Report Bugs and Feature Requests](#report-bugs-and-feature-requests) - [Tutorial](#tutorial)
- [Donations](#donations) - [Build from Source](#build-from-source)
- [Infrastructure](#infrastructure) - [Contributing](#contributing)
- [Contributors Wall](#contributors-wall) - [Report Bugs and Feature Requests](#report-bugs-and-feature-requests)
- [License](#license) - [Donations](#donations)
- [💸 PayPal](#-paypal)
- [Infrastructure](#infrastructure)
- [Contributors Wall](#contributors-wall)
- [License](#license)
## Features ## Features
@ -53,19 +57,23 @@ You have complete control over what goes into your resume, how it looks, what co
- Arabic (اَلْعَرَبِيَّةُ) - Arabic (اَلْعَرَبِيَّةُ)
- Bengali (বাংলা) - Bengali (বাংলা)
- Chinese (中文) - Chinese (中文)
- Czech (čeština)
- Danish (Dansk) - Danish (Dansk)
- Dutch (Nederlands) - Dutch (Nederlands)
- English - English
- French (Français) - French (Français)
- German (Deutsch) - German (Deutsch)
- Greek (Ελληνικά)
- Hindi (हिन्दी) - Hindi (हिन्दी)
- Italian (Italiano) - Italian (Italiano)
- Kannada (ಕನ್ನಡ) - Kannada (ಕನ್ನಡ)
- Malayalam (മലയാളം) - Malayalam (മലയാളം)
- Odia (ଓଡ଼ିଆ)
- Polish (Polski) - Polish (Polski)
- Portuguese (Português) - Portuguese (Português)
- Russian (русский) - Russian (русский)
- Spanish (Español) - Spanish (Español)
- Swedish (Svenska)
- Tamil (தமிழ்) - Tamil (தமிழ்)
- Turkish (Türkçe) - Turkish (Türkçe)
- Vietnamese (Tiếng Việt) - Vietnamese (Tiếng Việt)

View File

@ -1,4 +1,4 @@
FROM node:17-alpine as dependencies FROM node:lts-alpine as dependencies
RUN apk add --no-cache curl g++ make python3 \ RUN apk add --no-cache curl g++ make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
@ -11,7 +11,7 @@ COPY ./client/package.json ./client/package.json
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
FROM node:17-alpine as builder FROM node:lts-alpine as builder
RUN apk add --no-cache curl g++ make python3 \ RUN apk add --no-cache curl g++ make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
@ -27,7 +27,7 @@ COPY --from=dependencies /app/client/node_modules ./client/node_modules
RUN pnpm run build:schema RUN pnpm run build:schema
RUN pnpm run build:client RUN pnpm run build:client
FROM node:17-alpine as production FROM node:lts-alpine as production
WORKDIR /app WORKDIR /app

View File

@ -81,14 +81,14 @@ const LeftSidebar = () => {
arrow arrow
key={id} key={id}
placement="right" placement="right"
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`))} title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`)) as string}
> >
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton> <IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
</Tooltip> </Tooltip>
))} ))}
{customSections.map(({ id }) => ( {customSections.map(({ id }) => (
<Tooltip key={id} title={get(sections, `${id}.name`, '')} placement="right" arrow> <Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
<IconButton onClick={() => handleClick(id)}> <IconButton onClick={() => handleClick(id)}>
<Star /> <Star />
</IconButton> </IconButton>

View File

@ -57,6 +57,12 @@ const Basics = () => {
</div> </div>
</div> </div>
<ResumeInput
type="date"
label={t<string>('builder.leftSidebar.sections.basics.birthdate.label')}
path="basics.birthdate"
className="sm:col-span-2"
/>
<ResumeInput <ResumeInput
label={t<string>('builder.common.form.email.label')} label={t<string>('builder.common.form.email.label')}
path="basics.email" path="basics.email"

View File

@ -117,7 +117,7 @@ const Layout = () => {
[styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true), [styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true),
})} })}
> >
{get(resumeSections, `${sectionId}.name`, '')} {get(resumeSections, `${sectionId}.name`, '') as string}
</div> </div>
</div> </div>
)} )}

View File

@ -5,11 +5,12 @@ import { useRouter } from 'next/router';
import styles from './BaseModal.module.scss'; import styles from './BaseModal.module.scss';
type Props = { type Props = {
icon?: React.ReactNode;
isOpen: boolean; isOpen: boolean;
heading: string; heading: string;
handleClose: () => void; icon?: React.ReactNode;
children?: React.ReactNode;
footerChildren?: React.ReactNode; footerChildren?: React.ReactNode;
handleClose: () => void;
}; };
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => { const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {

View File

@ -76,6 +76,7 @@ const List: React.FC<Props> = ({
return ( return (
<ListItem <ListItem
key={item.id} key={item.id}
path={path}
item={item} item={item}
index={index} index={index}
title={title} title={title}

View File

@ -17,6 +17,7 @@ interface DragItem {
type Props = { type Props = {
item: ListItemType; item: ListItemType;
path: string;
index: number; index: number;
title: string; title: string;
subtitle?: string; subtitle?: string;
@ -26,14 +27,14 @@ type Props = {
onDuplicate?: (item: ListItemType) => void; onDuplicate?: (item: ListItemType) => void;
}; };
const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => { const ListItem: React.FC<Props> = ({ item, path, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<Element | null>(null); const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({ const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
accept: 'ListItem', accept: path,
collect(monitor) { collect(monitor) {
return { handlerId: monitor.getHandlerId() }; return { handlerId: monitor.getHandlerId() };
}, },
@ -68,7 +69,7 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
}); });
const [{ isDragging }, drag] = useDrag({ const [{ isDragging }, drag] = useDrag({
type: 'ListItem', type: path,
item: () => { item: () => {
return { id: item.id, index }; return { id: item.id, index };
}, },

View File

@ -1,5 +0,0 @@
import dynamic from 'next/dynamic';
const NoSSR: React.FC = ({ children }) => <>{children}</>;
export default dynamic(() => Promise.resolve(NoSSR), { ssr: false });

View File

@ -1,4 +1,7 @@
import { DatePicker } from '@mui/lab';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import dayjs from 'dayjs';
import { isEmpty } from 'lodash';
import get from 'lodash/get'; import get from 'lodash/get';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -8,7 +11,7 @@ import { setResumeState } from '@/store/resume/resumeSlice';
import MarkdownSupported from './MarkdownSupported'; import MarkdownSupported from './MarkdownSupported';
interface Props { interface Props {
type?: 'text' | 'textarea'; type?: 'text' | 'textarea' | 'date';
label: string; label: string;
path: string; path: string;
className?: string; className?: string;
@ -31,6 +34,11 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
dispatch(setResumeState({ path, value: event.target.value })); dispatch(setResumeState({ path, value: event.target.value }));
}; };
const onChangeValue = (value: string) => {
setValue(value);
dispatch(setResumeState({ path, value }));
};
if (type === 'textarea') { if (type === 'textarea') {
return ( return (
<TextField <TextField
@ -45,6 +53,22 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
); );
} }
if (type === 'date') {
return (
<DatePicker
openTo="year"
label={label}
value={value}
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).isValid() && onChangeValue(date.toISOString());
}}
/>
);
}
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />; return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
}; };

View File

@ -7,8 +7,10 @@ export type Language = {
export const languages: Language[] = [ export const languages: Language[] = [
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ' }, { code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ' },
{ code: 'bn', name: 'Bengali', localName: 'বাংলা' }, { code: 'bn', name: 'Bengali', localName: 'বাংলা' },
{ code: 'cs', name: 'Czech', localName: 'čeština' },
{ code: 'da', name: 'Danish', localName: 'Dansk' }, { code: 'da', name: 'Danish', localName: 'Dansk' },
{ code: 'de', name: 'German', localName: 'Deutsch' }, { code: 'de', name: 'German', localName: 'Deutsch' },
{ code: 'el', name: 'Greek', localName: 'Ελληνικά' },
{ code: 'en', name: 'English' }, { code: 'en', name: 'English' },
{ code: 'es', name: 'Spanish', localName: 'Español' }, { code: 'es', name: 'Spanish', localName: 'Español' },
{ code: 'fr', name: 'French', localName: 'Français' }, { code: 'fr', name: 'French', localName: 'Français' },
@ -17,9 +19,11 @@ export const languages: Language[] = [
{ code: 'kn', name: 'Kannada', localName: 'ಕನ್ನಡ' }, { code: 'kn', name: 'Kannada', localName: 'ಕನ್ನಡ' },
{ code: 'ml', name: 'Malayalam', localName: 'മലയാളം' }, { code: 'ml', name: 'Malayalam', localName: 'മലയാളം' },
{ code: 'nl', name: 'Dutch', localName: 'Nederlands' }, { code: 'nl', name: 'Dutch', localName: 'Nederlands' },
{ code: 'or', name: 'Odia', localName: 'ଓଡ଼ିଆ' },
{ code: 'pl', name: 'Polish', localName: 'Polski' }, { code: 'pl', name: 'Polish', localName: 'Polski' },
{ code: 'pt', name: 'Portuguese', localName: 'Português' }, { code: 'pt', name: 'Portuguese', localName: 'Português' },
{ code: 'ru', name: 'Russian', localName: 'русский' }, { code: 'ru', name: 'Russian', localName: 'русский' },
{ code: 'sv', name: 'Swedish', localName: 'Svenska' },
{ code: 'ta', name: 'Tamil', localName: 'தமிழ்' }, { code: 'ta', name: 'Tamil', localName: 'தமிழ்' },
{ code: 'tr', name: 'Turkish', localName: 'Türkçe' }, { code: 'tr', name: 'Turkish', localName: 'Türkçe' },
{ code: 'vi', name: 'Vietnamese', localName: 'Tiếng Việt' }, { code: 'vi', name: 'Vietnamese', localName: 'Tiếng Việt' },

View File

@ -6,7 +6,6 @@ import Joi from 'joi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Trans, useTranslation } from 'next-i18next'; import { Trans, useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useIsMutating, useMutation } from 'react-query'; import { useIsMutating, useMutation } from 'react-query';
@ -18,6 +17,8 @@ import { ServerError } from '@/services/axios';
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice'; import { setModalState } from '@/store/modal/modalSlice';
declare const google: any;
type FormData = { type FormData = {
identifier: string; identifier: string;
password: string; password: string;
@ -56,15 +57,6 @@ const LoginModal: React.FC = () => {
loginWithGoogle loginWithGoogle
); );
const { signIn } = useGoogleLogin({
clientId: env('GOOGLE_CLIENT_ID'),
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
handleClose();
},
});
const handleClose = () => { const handleClose = () => {
dispatch(setModalState({ modal: 'auth.login', state: { open: false } })); dispatch(setModalState({ modal: 'auth.login', state: { open: false } }));
reset(); reset();
@ -93,8 +85,18 @@ const LoginModal: React.FC = () => {
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } })); dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
}; };
const handleLoginWithGoogle = () => { const handleLoginWithGoogle = async () => {
signIn(); google.accounts.id.initialize({
client_id: env('GOOGLE_CLIENT_ID'),
callback: async (response: any) => {
await loginWithGoogleMutation({ credential: response.credential });
handleClose();
},
auto_select: false,
});
google.accounts.id.prompt();
}; };
const PasswordVisibility = (): React.ReactElement => { const PasswordVisibility = (): React.ReactElement => {

View File

@ -5,7 +5,6 @@ import { Button, TextField } from '@mui/material';
import Joi from 'joi'; import Joi from 'joi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Trans, useTranslation } from 'next-i18next'; import { Trans, useTranslation } from 'next-i18next';
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
@ -15,6 +14,8 @@ import { ServerError } from '@/services/axios';
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice'; import { setModalState } from '@/store/modal/modalSlice';
declare const google: any;
type FormData = { type FormData = {
name: string; name: string;
username: string; username: string;
@ -63,15 +64,6 @@ const RegisterModal: React.FC = () => {
loginWithGoogle loginWithGoogle
); );
const { signIn } = useGoogleLogin({
clientId: env('GOOGLE_CLIENT_ID'),
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
handleClose();
},
});
const handleClose = () => { const handleClose = () => {
dispatch(setModalState({ modal: 'auth.register', state: { open: false } })); dispatch(setModalState({ modal: 'auth.register', state: { open: false } }));
reset(); reset();
@ -87,8 +79,18 @@ const RegisterModal: React.FC = () => {
dispatch(setModalState({ modal: 'auth.login', state: { open: true } })); dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
}; };
const handleLoginWithGoogle = () => { const handleLoginWithGoogle = async () => {
signIn(); google.accounts.id.initialize({
client_id: env('GOOGLE_CLIENT_ID'),
callback: async (response: any) => {
await loginWithGoogleMutation({ credential: response.credential });
handleClose();
},
auto_select: false,
});
google.accounts.id.prompt();
}; };
return ( return (

View File

@ -13,67 +13,66 @@
"@emotion/react": "^11.9.0", "@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@hookform/resolvers": "2.8.8", "@hookform/resolvers": "2.8.8",
"@monaco-editor/react": "^4.4.1", "@monaco-editor/react": "^4.4.4",
"@mui/icons-material": "^5.6.0", "@mui/icons-material": "^5.6.2",
"@mui/lab": "^5.0.0-alpha.76", "@mui/lab": "^5.0.0-alpha.79",
"@mui/material": "^5.6.0", "@mui/material": "^5.6.3",
"@reduxjs/toolkit": "^1.8.1", "@reduxjs/toolkit": "^1.8.1",
"axios": "^0.26.1", "axios": "^0.27.2",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"dayjs": "^1.11.0", "dayjs": "^1.11.1",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"joi": "^17.6.0", "joi": "^17.6.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"md5-hex": "^4.0.0", "md5-hex": "^4.0.0",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nanoid": "^3.3.2", "nanoid": "^3.3.3",
"next": "12.1.4", "next": "12.1.5",
"next-i18next": "^11.0.0", "next-i18next": "^11.0.0",
"react": "<18", "react": "^18",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-colorful": "^5.5.1", "react-colorful": "^5.5.1",
"react-dnd": "^15.1.2", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^15.1.2", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "<18", "react-dom": "^18",
"react-google-login": "^5.2.2", "react-hook-form": "^7.30.0",
"react-hook-form": "^7.29.0",
"react-hot-toast": "2.2.0", "react-hot-toast": "2.2.0",
"react-hotkeys-hook": "^3.4.4", "react-hotkeys-hook": "^3.4.4",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-markdown": "^8.0.2", "react-markdown": "^8.0.3",
"react-query": "^3.34.19", "react-query": "^3.38.0",
"react-redux": "^7.2.8", "react-redux": "^8.0.1",
"react-zoom-pan-pinch": "^2.1.3", "react-zoom-pan-pinch": "^2.1.3",
"redux": "^4.1.2", "redux": "^4.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.1.3", "redux-saga": "^1.1.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sharp": "^0.30.3", "sharp": "^0.30.4",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "^1.6.28" "webfontloader": "^1.6.28"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.9", "@babel/core": "^7.17.10",
"@reactive-resume/schema": "workspace:*", "@reactive-resume/schema": "workspace:*",
"@tailwindcss/typography": "^0.5.2", "@tailwindcss/typography": "^0.5.2",
"@types/downloadjs": "^1.4.3", "@types/downloadjs": "^1.4.3",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.182",
"@types/node": "17.0.23", "@types/node": "17.0.30",
"@types/react": "<18", "@types/react": "^18",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-redux": "^7.1.23", "@types/react-redux": "^7.1.24",
"@types/tailwindcss": "^3.0.10", "@types/tailwindcss": "^3.0.10",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/webfontloader": "^1.6.34", "@types/webfontloader": "^1.6.34",
"autoprefixer": "^10.4.4", "autoprefixer": "^10.4.5",
"csstype": "^3.0.11", "csstype": "^3.0.11",
"eslint": "^8.12.0", "eslint": "^8.14.0",
"eslint-config-next": "12.1.4", "eslint-config-next": "12.1.5",
"next-sitemap": "^2.5.19", "next-sitemap": "^2.5.20",
"postcss": "^8.4.12", "postcss": "^8.4.13",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"sass": "^1.50.0", "sass": "^1.51.0",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.24",
"typescript": "<4.6.0" "typescript": "^4.6.4"
} }
} }

View File

@ -4,6 +4,7 @@ import DateAdapter from '@mui/lab/AdapterDayjs';
import LocalizationProvider from '@mui/lab/LocalizationProvider'; import LocalizationProvider from '@mui/lab/LocalizationProvider';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
import Script from 'next/script';
import { appWithTranslation } from 'next-i18next'; import { appWithTranslation } from 'next-i18next';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
import { QueryClientProvider } from 'react-query'; import { QueryClientProvider } from 'react-query';
@ -52,6 +53,8 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
</PersistGate> </PersistGate>
</LocalizationProvider> </LocalizationProvider>
</ReduxProvider> </ReduxProvider>
<Script src="https://accounts.google.com/gsi/client" />
</> </>
); );
}; };

View File

@ -1,6 +1,6 @@
import { DarkMode, LightMode, Link as LinkIcon } from '@mui/icons-material'; import { DarkMode, LightMode, Link as LinkIcon } from '@mui/icons-material';
import { Masonry } from '@mui/lab'; import { Masonry } from '@mui/lab';
import { Button, IconButton } from '@mui/material'; import { Button, IconButton, NoSsr } from '@mui/material';
import type { GetStaticProps, NextPage } from 'next'; import type { GetStaticProps, NextPage } from 'next';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@ -11,7 +11,6 @@ import Testimony from '@/components/landing/Testimony';
import Footer from '@/components/shared/Footer'; import Footer from '@/components/shared/Footer';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher'; import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
import Logo from '@/components/shared/Logo'; import Logo from '@/components/shared/Logo';
import NoSSR from '@/components/shared/NoSSR';
import { screenshots } from '@/config/screenshots'; import { screenshots } from '@/config/screenshots';
import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags'; import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags';
import testimonials from '@/data/testimonials'; import testimonials from '@/data/testimonials';
@ -59,7 +58,7 @@ const Home: NextPage = () => {
<h2>{t<string>('common.subtitle')}</h2> <h2>{t<string>('common.subtitle')}</h2>
<NoSSR> <NoSsr>
<div className={styles.buttonWrapper}> <div className={styles.buttonWrapper}>
{isLoggedIn ? ( {isLoggedIn ? (
<> <>
@ -81,7 +80,7 @@ const Home: NextPage = () => {
</> </>
)} )}
</div> </div>
</NoSSR> </NoSsr>
</div> </div>
</div> </div>

View File

@ -119,6 +119,9 @@
"name": { "name": {
"label": "Full Name" "label": "Full Name"
}, },
"birthdate": {
"label": "Date of Birth"
},
"photo-filters": { "photo-filters": {
"effects": { "effects": {
"border": { "border": {

View File

@ -1,43 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://rxresu.me</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/meta/privacy</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/meta/privacy</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/meta/service</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/meta/service</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ar/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/ar/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/bn/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/bn/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/da/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/da/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/de/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/de/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/es/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/es/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/fr/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/fr/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/hi/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/hi/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/it/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/it/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/kn/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.972Z</lastmod></url> <url><loc>https://rxresu.me/kn/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ml/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ml/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/nl/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/nl/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/pl/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/pl/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/pt/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/pt/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ru/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ru/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ta/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ta/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/tr/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/tr/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/vi/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/vi/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/zh/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/zh/dashboard</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ar</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ar</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/bn</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/bn</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/da</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/da</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/de</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/de</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/es</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/es</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/fr</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/fr</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/hi</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/hi</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/it</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/it</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/kn</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/kn</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ml</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ml</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/nl</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/nl</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/pl</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/pl</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/pt</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/pt</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ru</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ru</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/ta</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/ta</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/tr</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/tr</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/vi</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/vi</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
<url><loc>https://rxresu.me/zh</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-08T08:32:21.973Z</lastmod></url> <url><loc>https://rxresu.me/zh</loc><changefreq>monthly</changefreq><priority>0.7</priority><lastmod>2022-04-30T10:56:16.825Z</lastmod></url>
</urlset> </urlset>

View File

@ -13,7 +13,7 @@ export type LoginParams = {
}; };
export type LoginWithGoogleParams = { export type LoginWithGoogleParams = {
accessToken: string; credential: string;
}; };
export type RegisterParams = { export type RegisterParams = {

View File

@ -1,4 +1,4 @@
import _axios, { AxiosError } from 'axios'; import _axios from 'axios';
import Router from 'next/router'; import Router from 'next/router';
import { logout } from '@/store/auth/authSlice'; import { logout } from '@/store/auth/authSlice';
@ -27,7 +27,7 @@ axios.interceptors.request.use((config) => {
axios.interceptors.response.use( axios.interceptors.response.use(
(response) => response, (response) => response,
(error: AxiosError) => { (error) => {
const { response } = error; const { response } = error;
if (response) { if (response) {

View File

@ -5,7 +5,7 @@ import { useMemo } from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]); const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]);

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import { Theme } from '@reactive-resume/schema'; import { Theme } from '@reactive-resume/schema';
import clsx from 'clsx'; import clsx from 'clsx';
import get from 'lodash/get'; import get from 'lodash/get';
@ -9,12 +9,14 @@ import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { getContrastColor } from '@/utils/styles'; import { getContrastColor } from '@/utils/styles';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const { name, headline, photo, email, phone, website, location, profiles } = useAppSelector( const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.basics
); );
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
@ -43,6 +45,10 @@ export const MastheadSidebar: React.FC = () => {
{formatLocation(location)} {formatLocation(location)}
</DataDisplay> </DataDisplay>
<DataDisplay icon={<Cake />} className="!gap-2 text-xs">
{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}`}>
{email} {email}
</DataDisplay> </DataDisplay>

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
return ( return (

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import { alpha } from '@mui/material'; import { alpha } from '@mui/material';
import { Theme } from '@reactive-resume/schema'; import { Theme } from '@reactive-resume/schema';
import clsx from 'clsx'; import clsx from 'clsx';
@ -10,12 +10,14 @@ import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { getContrastColor } from '@/utils/styles'; import { getContrastColor } from '@/utils/styles';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const { name, headline, photo, email, phone, website, location, profiles } = useAppSelector( const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.basics
); );
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
@ -44,6 +46,10 @@ export const MastheadSidebar: React.FC = () => {
{formatLocation(location)} {formatLocation(location)}
</DataDisplay> </DataDisplay>
<DataDisplay icon={<Cake />} className="!gap-2 text-xs">
{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}`}>
{email} {email}
</DataDisplay> </DataDisplay>

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
return ( return (

View File

@ -1,16 +1,18 @@
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import get from 'lodash/get'; import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary'));
const { name, headline, photo, email, phone, website, location, profiles } = useAppSelector( const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.basics
); );
@ -36,6 +38,10 @@ export const MastheadSidebar: React.FC = () => {
{formatLocation(location)} {formatLocation(location)}
</DataDisplay> </DataDisplay>
<DataDisplay icon={<Cake />} className="text-xs">
{formatDateString(birthdate, dateFormat)}
</DataDisplay>
<DataDisplay icon={<Email />} className="text-xs" link={`mailto:${email}`}> <DataDisplay icon={<Email />} className="text-xs" link={`mailto:${email}`}>
{email} {email}
</DataDisplay> </DataDisplay>

View File

@ -1,4 +1,4 @@
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
return <h3 className="my-2 inline-block border-b px-5 pb-2">{children}</h3>; return <h3 className="my-2 inline-block border-b px-5 pb-2">{children}</h3>;
}; };

View File

@ -1,14 +1,17 @@
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import React from 'react'; import React from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
const Masthead = () => { const Masthead = () => {
const { name, photo, email, phone, website, headline, location, profiles } = useAppSelector( const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const { name, photo, email, phone, website, birthdate, headline, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.basics
); );
@ -32,6 +35,8 @@ const Masthead = () => {
</div> </div>
<div className="flex flex-wrap justify-center gap-3"> <div className="flex flex-wrap justify-center gap-3">
<DataDisplay icon={<Cake />}>{formatDateString(birthdate, dateFormat)}</DataDisplay>
<DataDisplay icon={<Email />} link={`mailto:${email}`}> <DataDisplay icon={<Email />} link={`mailto:${email}`}>
{email} {email}
</DataDisplay> </DataDisplay>

View File

@ -1,5 +1,3 @@
.page {}
.container { .container {
@apply grid grid-cols-2 gap-8 px-6 py-4; @apply grid grid-cols-2 gap-8 px-6 py-4;

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
return ( return (

View File

@ -1,4 +1,4 @@
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import { alpha } from '@mui/material'; import { alpha } from '@mui/material';
import { Theme } from '@reactive-resume/schema'; import { Theme } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
@ -6,11 +6,13 @@ import isEmpty from 'lodash/isEmpty';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
const Masthead: React.FC = () => { const Masthead: React.FC = () => {
const { name, photo, headline, summary, email, phone, website, location, profiles } = useAppSelector( const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.basics
); );
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
@ -48,6 +50,12 @@ const Masthead: React.FC = () => {
id="Masterhead_data" id="Masterhead_data"
style={{ backgroundColor: alpha(theme.primary, 0.4), gridTemplateColumns: `repeat(2, minmax(0, 1fr))` }} style={{ backgroundColor: alpha(theme.primary, 0.4), gridTemplateColumns: `repeat(2, minmax(0, 1fr))` }}
> >
<DataDisplay icon={<Room />} className="col-span-2">
{formatLocation(location)}
</DataDisplay>
<DataDisplay icon={<Cake />}>{formatDateString(birthdate, dateFormat)}</DataDisplay>
<DataDisplay icon={<Email />} link={`mailto:${email}`}> <DataDisplay icon={<Email />} link={`mailto:${email}`}>
{email} {email}
</DataDisplay> </DataDisplay>
@ -60,8 +68,6 @@ const Masthead: React.FC = () => {
{website} {website}
</DataDisplay> </DataDisplay>
<DataDisplay icon={<Room />}>{formatLocation(location)}</DataDisplay>
{profiles.map(({ id, username, network, url }) => ( {profiles.map(({ id, username, network, url }) => (
<DataDisplay key={id} icon={getProfileIcon(network)} link={url && addHttp(url)}> <DataDisplay key={id} icon={getProfileIcon(network)} link={url && addHttp(url)}>
{username} {username}

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
return ( return (

View File

@ -1,13 +1,16 @@
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
const Masthead: React.FC = () => { const Masthead: React.FC = () => {
const { name, photo, email, phone, website, headline, location, profiles } = useAppSelector( const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const { name, photo, email, phone, website, birthdate, headline, location, profiles } = useAppSelector(
(state) => state.resume.basics (state) => state.resume.basics
); );
@ -33,6 +36,10 @@ const Masthead: React.FC = () => {
</DataDisplay> </DataDisplay>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DataDisplay icon={<Cake />} className="text-xs">
{formatDateString(birthdate, dateFormat)}
</DataDisplay>
<DataDisplay icon={<Email />} className="text-xs" link={`mailto:${email}`}> <DataDisplay icon={<Email />} className="text-xs" link={`mailto:${email}`}>
{email} {email}
</DataDisplay> </DataDisplay>

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const Heading: React.FC = ({ children }) => { const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {}));
return ( return (

View File

@ -1,4 +1,4 @@
import { Email, Phone, Public, Room } from '@mui/icons-material'; import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import { Theme } from '@reactive-resume/schema'; import { Theme } from '@reactive-resume/schema';
import get from 'lodash/get'; import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
@ -7,12 +7,16 @@ import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown'; import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import DataDisplay from '@/templates/shared/DataDisplay'; import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import getProfileIcon from '@/utils/getProfileIcon'; import getProfileIcon from '@/utils/getProfileIcon';
import { getContrastColor } from '@/utils/styles'; import { getContrastColor } from '@/utils/styles';
import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template';
export const MastheadSidebar: React.FC = () => { export const MastheadSidebar: React.FC = () => {
const { name, photo, email, phone, website, location, profiles } = useAppSelector((state) => state.resume.basics); const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format'));
const { name, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.basics
);
return ( return (
<div className="col-span-2 grid justify-items-left gap-4"> <div className="col-span-2 grid justify-items-left gap-4">
@ -31,6 +35,10 @@ export const MastheadSidebar: React.FC = () => {
{formatLocation(location)} {formatLocation(location)}
</DataDisplay> </DataDisplay>
<DataDisplay icon={<Cake />} className="text-xs">
{formatDateString(birthdate, dateFormat)}
</DataDisplay>
<DataDisplay icon={<Email />} className="text-xs" link={`mailto:${email}`}> <DataDisplay icon={<Email />} className="text-xs" link={`mailto:${email}`}>
{email} {email}
</DataDisplay> </DataDisplay>

View File

@ -7,7 +7,7 @@ type Props = {
className?: string; className?: string;
}; };
const DataDisplay: React.FC<Props> = ({ icon, link, className, children }) => { const DataDisplay: React.FC<React.PropsWithChildren<Props>> = ({ icon, link, className, children }) => {
if (isEmpty(children)) return null; if (isEmpty(children)) return null;
if (!isEmpty(link)) { if (!isEmpty(link)) {

View File

@ -6,6 +6,7 @@ import {
FaFacebookF, FaFacebookF,
FaGithub, FaGithub,
FaGitlab, FaGitlab,
FaHackerrank,
FaInstagram, FaInstagram,
FaLinkedinIn, FaLinkedinIn,
FaSkype, FaSkype,
@ -16,20 +17,24 @@ import {
FaXing, FaXing,
FaYoutube, FaYoutube,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { SiCodechef, SiCodeforces } from 'react-icons/si';
const profileIconMap: Record<string, JSX.Element> = { const profileIconMap: Record<string, JSX.Element> = {
facebook: <FaFacebookF />,
twitter: <FaTwitter />,
linkedin: <FaLinkedinIn />,
dribbble: <FaDribbble />,
soundcloud: <FaSoundcloud />,
github: <FaGithub />,
instagram: <FaInstagram />,
stackoverflow: <FaStackOverflow />,
behance: <FaBehance />, behance: <FaBehance />,
codechef: <SiCodechef />,
codeforces: <SiCodeforces />,
dribbble: <FaDribbble />,
facebook: <FaFacebookF />,
github: <FaGithub />,
gitlab: <FaGitlab />, gitlab: <FaGitlab />,
telegram: <FaTelegram />, hackerrank: <FaHackerrank />,
instagram: <FaInstagram />,
linkedin: <FaLinkedinIn />,
skype: <FaSkype />, skype: <FaSkype />,
soundcloud: <FaSoundcloud />,
stackoverflow: <FaStackOverflow />,
telegram: <FaTelegram />,
twitter: <FaTwitter />,
xing: <FaXing />, xing: <FaXing />,
youtube: <FaYoutube />, youtube: <FaYoutube />,
}; };

View File

@ -3,7 +3,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
const DateWrapper: React.FC = ({ children }) => { const DateWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const { locale } = useRouter(); const { locale } = useRouter();
useEffect(() => { useEffect(() => {
@ -12,8 +12,10 @@ const DateWrapper: React.FC = ({ children }) => {
// Locales // Locales
require('dayjs/locale/ar'); require('dayjs/locale/ar');
require('dayjs/locale/bn'); require('dayjs/locale/bn');
require('dayjs/locale/cs');
require('dayjs/locale/da'); require('dayjs/locale/da');
require('dayjs/locale/de'); require('dayjs/locale/de');
require('dayjs/locale/el');
require('dayjs/locale/en'); require('dayjs/locale/en');
require('dayjs/locale/es'); require('dayjs/locale/es');
require('dayjs/locale/fr'); require('dayjs/locale/fr');
@ -25,6 +27,7 @@ const DateWrapper: React.FC = ({ children }) => {
require('dayjs/locale/pl'); require('dayjs/locale/pl');
require('dayjs/locale/pt'); require('dayjs/locale/pt');
require('dayjs/locale/ru'); require('dayjs/locale/ru');
require('dayjs/locale/sv');
require('dayjs/locale/ta'); require('dayjs/locale/ta');
require('dayjs/locale/tr'); require('dayjs/locale/tr');
require('dayjs/locale/vi'); require('dayjs/locale/vi');

View File

@ -4,7 +4,7 @@ import { useCallback, useEffect } from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
const FontWrapper: React.FC = ({ children }) => { const FontWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const typography = useAppSelector((state) => get(state.resume, 'metadata.typography')); const typography = useAppSelector((state) => get(state.resume, 'metadata.typography'));
const loadFonts = useCallback(async () => { const loadFonts = useCallback(async () => {

View File

@ -3,7 +3,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { toggleSidebar } from '@/store/build/buildSlice'; import { toggleSidebar } from '@/store/build/buildSlice';
import { useAppDispatch } from '@/store/hooks'; import { useAppDispatch } from '@/store/hooks';
const HotkeysWrapper: React.FC = ({ children }) => { const HotkeysWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useHotkeys('ctrl+/, cmd+/', () => { useHotkeys('ctrl+/, cmd+/', () => {

View File

@ -5,7 +5,7 @@ import { darkTheme, lightTheme } from '@/config/theme';
import { setTheme } from '@/store/build/buildSlice'; import { setTheme } from '@/store/build/buildSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { useAppDispatch, useAppSelector } from '@/store/hooks';
const ThemeWrapper: React.FC = ({ children }) => { const ThemeWrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const theme = useAppSelector((state) => state.build.theme); const theme = useAppSelector((state) => state.build.theme);

View File

@ -3,7 +3,7 @@ import FontWrapper from './FontWrapper';
import HotkeysWrapper from './HotkeysWrapper'; import HotkeysWrapper from './HotkeysWrapper';
import ThemeWrapper from './ThemeWrapper'; import ThemeWrapper from './ThemeWrapper';
const WrapperRegistry: React.FC = ({ children }) => { const WrapperRegistry: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
return ( return (
<ThemeWrapper> <ThemeWrapper>
<FontWrapper> <FontWrapper>

View File

@ -47,19 +47,23 @@ You have complete control over what goes into your resume, how it looks, what co
- Arabic (اَلْعَرَبِيَّةُ) - Arabic (اَلْعَرَبِيَّةُ)
- Bengali (বাংলা) - Bengali (বাংলা)
- Chinese (中文) - Chinese (中文)
- Czech (čeština)
- Danish (Dansk) - Danish (Dansk)
- Dutch (Nederlands) - Dutch (Nederlands)
- English - English
- French (Français) - French (Français)
- German (Deutsch) - German (Deutsch)
- Greek (Ελληνικά)
- Hindi (हिन्दी) - Hindi (हिन्दी)
- Italian (Italiano) - Italian (Italiano)
- Kannada (ಕನ್ನಡ) - Kannada (ಕನ್ನಡ)
- Malayalam (മലയാളം) - Malayalam (മലയാളം)
- Odia (ଓଡ଼ିଆ)
- Polish (Polski) - Polish (Polski)
- Portuguese (Português) - Portuguese (Português)
- Russian (русский) - Russian (русский)
- Spanish (Español) - Spanish (Español)
- Swedish (Svenska)
- Tamil (தமிழ்) - Tamil (தமிழ்)
- Turkish (Türkçe) - Turkish (Türkçe)
- Vietnamese (Tiếng Việt) - Vietnamese (Tiếng Việt)

View File

@ -127,13 +127,15 @@ This field is only required if the Google Login functionality is important to yo
### `GOOGLE_API_KEY` ### `GOOGLE_API_KEY`
**Required**: `yes` **Required**: `no`
**Description:** Google API Key used for fetching Google Fonts **Description:** Google API Key used for fetching Google Fonts
Within the resume builder, there's a section where you can pick any font from the Google Fonts Library. To fetch the names and IDs of these fonts, we depend on the Google Fonts API. It does not cost any payment, or the need to enter credit card information to create or use this API. Within the resume builder, there's a section where you can pick any font from the Google Fonts Library. To fetch the names and IDs of these fonts, we depend on the Google Fonts API. It does not cost any payment, or the need to enter credit card information to create or use this API.
You can get your own key here: https://developers.google.com/fonts/docs/developer_api You can get your own key here: https://developers.google.com/fonts/docs/developer_api
If you do not have a Google API Key, it was make use of the cached response JSON that's stored within the project source. Please note that this cache is not updated and may not have all the latest fonts that Google Fonts has to offer.
## SendGrid ## SendGrid
The server makes use of SendGrid to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.** The server makes use of SendGrid to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.**

View File

@ -17,11 +17,11 @@
"@algolia/client-search": "^4.13.0", "@algolia/client-search": "^4.13.0",
"@docusaurus/core": "2.0.0-beta.18", "@docusaurus/core": "2.0.0-beta.18",
"@docusaurus/preset-classic": "2.0.0-beta.18", "@docusaurus/preset-classic": "2.0.0-beta.18",
"@mdx-js/react": "^2.1.1", "@mdx-js/react": "1.6.22",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"prism-react-renderer": "^1.3.1", "prism-react-renderer": "^1.3.1",
"react": "<18", "react": "17.0.2",
"react-dom": "<18" "react-dom": "17.0.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -36,6 +36,6 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/react": "<18" "@types/react": "17.0.2"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "reactive-resume", "name": "reactive-resume",
"version": "3.3.4", "version": "3.4.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"schema", "schema",
@ -38,10 +38,10 @@
"env-cmd": "^10.1.0" "env-cmd": "^10.1.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.18.0", "@typescript-eslint/parser": "^5.21.0",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"eslint": "^8.12.0", "eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
@ -50,6 +50,6 @@
"husky": "^7.0.4", "husky": "^7.0.4",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"standard-version": "^9.3.2", "standard-version": "^9.3.2",
"typescript": "<4.6.0" "typescript": "^4.6.4"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"lint": "eslint --fix --ext .ts ./src" "lint": "eslint --fix --ext .ts ./src"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.12.0", "eslint": "^8.14.0",
"typescript": "<4.6.0" "typescript": "^4.6.4"
} }
} }

View File

@ -35,6 +35,7 @@ export type Basics = {
phone: string; phone: string;
website: string; website: string;
headline: string; headline: string;
birthdate: string;
summary: string; summary: string;
location: Location; location: Location;
profiles: Profile[]; profiles: Profile[];

View File

@ -1,4 +1,4 @@
FROM node:17-alpine as dependencies FROM node:lts-alpine as dependencies
RUN apk add --no-cache g++ curl make python3 \ RUN apk add --no-cache g++ curl make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
@ -11,7 +11,7 @@ COPY ./server/package.json ./server/package.json
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
FROM node:17-alpine as builder FROM node:lts-alpine as builder
RUN apk add --no-cache g++ curl make python3 \ RUN apk add --no-cache g++ curl make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm

View File

@ -10,7 +10,7 @@
"lint": "eslint --fix --ext .ts ./src" "lint": "eslint --fix --ext .ts ./src"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.67.0", "@aws-sdk/client-s3": "^3.81.0",
"@nestjs/axios": "^0.0.7", "@nestjs/axios": "^0.0.7",
"@nestjs/common": "^8.4.4", "@nestjs/common": "^8.4.4",
"@nestjs/config": "^2.0.0", "@nestjs/config": "^2.0.0",
@ -31,23 +31,23 @@
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"csvtojson": "^2.0.10", "csvtojson": "^2.0.10",
"dayjs": "^1.11.0", "dayjs": "^1.11.1",
"googleapis": "^100.0.0", "google-auth-library": "^8.0.2",
"joi": "^17.6.0", "joi": "^17.6.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
"nanoid": "^3.3.2", "nanoid": "^3.3.3",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"passport": "^0.5.2", "passport": "^0.5.2",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pg": "^8.7.3", "pg": "^8.7.3",
"playwright-chromium": "^1.20.2", "playwright-chromium": "^1.21.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"typeorm": "^0.2.34", "typeorm": "0.2",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
@ -55,17 +55,17 @@
"@nestjs/schematics": "^8.0.10", "@nestjs/schematics": "^8.0.10",
"@reactive-resume/schema": "workspace:*", "@reactive-resume/schema": "workspace:*",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^17.0.23", "@types/node": "^17.0.30",
"eslint": "^8.12.0", "eslint": "^8.14.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-loader": "^9.2.8", "ts-loader": "^9.2.9",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"tsconfig-paths": "^3.14.1", "tsconfig-paths": "^3.14.1",
"typescript": "<4.6.0", "typescript": "^4.6.4",
"webpack": "^5.72.0" "webpack": "^5.72.0"
} }
} }

View File

@ -21,8 +21,8 @@ export class AuthController {
} }
@Post('google') @Post('google')
async loginWithGoogle(@Body('accessToken') googleAccessToken: string) { async loginWithGoogle(@Body('credential') credential: string) {
const user = await this.authService.authenticateWithGoogle(googleAccessToken); const user = await this.authService.authenticateWithGoogle(credential);
const accessToken = this.authService.getAccessToken(user.id); const accessToken = this.authService.getAccessToken(user.id);
return { user, accessToken }; return { user, accessToken };

View File

@ -4,7 +4,7 @@ import { JwtService } from '@nestjs/jwt';
import { SchedulerRegistry } from '@nestjs/schedule'; import { SchedulerRegistry } from '@nestjs/schedule';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { randomInt } from 'crypto'; import { randomInt } from 'crypto';
import { google } from 'googleapis'; import { OAuth2Client } from 'google-auth-library';
import { PostgresErrorCode } from '@/database/errorCodes.enum'; import { PostgresErrorCode } from '@/database/errorCodes.enum';
import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto'; import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto';
@ -107,17 +107,16 @@ export class AuthService {
return this.usersService.findById(payload.id); return this.usersService.findById(payload.id);
} }
async authenticateWithGoogle(googleAccessToken: string) { async authenticateWithGoogle(credential: string) {
const clientID = this.configService.get<string>('google.clientID'); const clientID = this.configService.get<string>('google.clientID');
const clientSecret = this.configService.get<string>('google.clientSecret'); const clientSecret = this.configService.get<string>('google.clientSecret');
const OAuthClient = new google.auth.OAuth2(clientID, clientSecret); const OAuthClient = new OAuth2Client(clientID, clientSecret);
OAuthClient.setCredentials({ access_token: googleAccessToken }); const client = await OAuthClient.verifyIdToken({ idToken: credential });
const userPayload = client.getPayload();
const { email } = await OAuthClient.getTokenInfo(googleAccessToken);
try { try {
const user = await this.usersService.findByEmail(email); const user = await this.usersService.findByEmail(userPayload.email);
return user; return user;
} catch (error: any) { } catch (error: any) {
@ -125,14 +124,12 @@ export class AuthService {
throw new HttpException(error, HttpStatus.BAD_GATEWAY); throw new HttpException(error, HttpStatus.BAD_GATEWAY);
} }
const UserInfoClient = google.oauth2('v2').userinfo; const username = userPayload.email.split('@')[0];
const { data } = await UserInfoClient.get({ auth: OAuthClient });
const username = data.email.split('@')[0];
const createUserDto: CreateGoogleUserDto = { const createUserDto: CreateGoogleUserDto = {
name: `${data.given_name} ${data.family_name}`, name: `${userPayload.given_name} ${userPayload.family_name}`,
username, username,
email: data.email, email: userPayload.email,
provider: 'google', provider: 'google',
}; };

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,8 @@ import { Font } from '@reactive-resume/schema';
import { get } from 'lodash'; import { get } from 'lodash';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import cachedResponse from './assets/cachedResponse.json';
@Injectable() @Injectable()
export class FontsService { export class FontsService {
constructor(private configService: ConfigService, private httpService: HttpService) {} constructor(private configService: ConfigService, private httpService: HttpService) {}
@ -13,8 +15,14 @@ export class FontsService {
const apiKey = this.configService.get<string>('google.apiKey'); const apiKey = this.configService.get<string>('google.apiKey');
const url = 'https://www.googleapis.com/webfonts/v1/webfonts?key=' + apiKey; const url = 'https://www.googleapis.com/webfonts/v1/webfonts?key=' + apiKey;
const response = await firstValueFrom(this.httpService.get(url)); let data = [];
const data = get(response.data, 'items', []);
if (apiKey) {
const response = await firstValueFrom(this.httpService.get(url));
data = get(response.data, 'items', []);
} else {
data = cachedResponse;
}
return data; return data;
} }

View File

@ -10,6 +10,7 @@ const defaultState: Partial<Resume> = {
basics: { basics: {
email: '', email: '',
headline: '', headline: '',
birthdate: '',
photo: { photo: {
url: '', url: '',
visible: true, visible: true,

View File

@ -5,6 +5,7 @@ const sampleData: Partial<Resume> = {
name: 'Alexis Jones', name: 'Alexis Jones',
email: 'alexis.jones@gmail.com', email: 'alexis.jones@gmail.com',
phone: '+1 800 1200 3820', phone: '+1 800 1200 3820',
birthdate: '1995-08-06T00:00:00.000Z',
photo: { photo: {
url: `/images/sample-photo.jpg`, url: `/images/sample-photo.jpg`,
filters: { filters: {

View File

@ -35,6 +35,8 @@ export class ResumeController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get() @Get()
async findAllByUser(@User('id') userId: number) { async findAllByUser(@User('id') userId: number) {
console.log('findAllByUser', userId);
return this.resumeService.findAllByUser(userId); return this.resumeService.findAllByUser(userId);
} }

View File

@ -15,6 +15,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": false, "strictNullChecks": false,
"noImplicitAny": false, "noImplicitAny": false,
"resolveJsonModule": true,
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false, "noFallthroughCasesInSwitch": false,