🚀 release v3.0.0

This commit is contained in:
Amruth Pillai
2022-03-06 22:48:29 +01:00
parent 00505a9e5d
commit 9c1380f401
373 changed files with 12050 additions and 15783 deletions

View File

@ -0,0 +1,33 @@
import { User } from '@reactive-resume/schema';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type AuthState = {
user: User | null;
accessToken: string | null;
isLoggedIn: boolean;
};
const initialState: AuthState = {
user: null,
accessToken: null,
isLoggedIn: false,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
},
setAccessToken: (state, action: PayloadAction<string>) => {
state.accessToken = action.payload;
state.isLoggedIn = true;
},
logout: () => initialState,
},
});
export const { setUser, setAccessToken, logout } = authSlice.actions;
export default authSlice.reducer;

View File

@ -0,0 +1,70 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import merge from 'lodash/merge';
export type Theme = 'light' | 'dark';
export type Sidebar = 'left' | 'right';
export type SidebarState = { open: boolean };
export type Orientation = 'horizontal' | 'vertical';
export type BuildState = {
theme?: Theme;
sidebar: Record<Sidebar, SidebarState>;
page: {
breakLine: boolean;
orientation: Orientation;
};
};
const initialState: BuildState = {
sidebar: {
left: { open: false },
right: { open: false },
},
page: {
breakLine: true,
orientation: 'horizontal',
},
};
type SetThemePayload = { theme: Theme };
type ToggleSidebarPayload = { sidebar: Sidebar };
type SetSidebarStatePayload = { sidebar: Sidebar; state: SidebarState };
export const buildSlice = createSlice({
name: 'build',
initialState,
reducers: {
setTheme: (state, action: PayloadAction<SetThemePayload>) => {
const { theme } = action.payload;
state.theme = theme;
},
toggleSidebar: (state, action: PayloadAction<ToggleSidebarPayload>) => {
const { sidebar } = action.payload;
state.sidebar[sidebar].open = !state.sidebar[sidebar].open;
},
setSidebarState: (state, action: PayloadAction<SetSidebarStatePayload>) => {
const { sidebar, state: newState } = action.payload;
state.sidebar[sidebar] = merge(state.sidebar[sidebar], newState);
},
togglePageBreakLine: (state) => {
state.page.breakLine = !state.page.breakLine;
},
togglePageOrientation: (state) => {
const orientation: Orientation = state.page.orientation === 'horizontal' ? 'vertical' : 'horizontal';
state.page.orientation = orientation;
},
},
});
export const { setTheme, toggleSidebar, setSidebarState, togglePageBreakLine, togglePageOrientation } =
buildSlice.actions;
export default buildSlice.reducer;

6
client/store/hooks.ts Normal file
View File

@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from '@/store/index';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

44
client/store/index.ts Normal file
View File

@ -0,0 +1,44 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import createSagaMiddleware from 'redux-saga';
import authReducer from '@/store/auth/authSlice';
import buildReducer from '@/store/build/buildSlice';
import modalReducer from '@/store/modal/modalSlice';
import resumeReducer from '@/store/resume/resumeSlice';
import syncSaga from './sagas/sync';
import storage from './storage';
const sagaMiddleware = createSagaMiddleware();
const reducers = combineReducers({
auth: authReducer,
modal: modalReducer,
build: buildReducer,
resume: resumeReducer,
});
const persistedReducers = persistReducer({ key: 'root', storage, whitelist: ['auth', 'build'] }, reducers);
const store = configureStore({
reducer: persistedReducers,
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST'],
},
}).concat(sagaMiddleware);
},
});
sagaMiddleware.run(syncSaga);
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;

View File

@ -0,0 +1,56 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export type ModalName =
| 'auth.login'
| 'auth.register'
| 'auth.forgot'
| 'auth.reset'
| 'dashboard.create-resume'
| 'dashboard.import-external'
| 'dashboard.rename-resume'
| 'builder.sections.profile'
| `builder.sections.${string}`;
export type ModalState = {
open: boolean;
payload?: { path?: string; item?: any; onComplete?: (newItem: any) => void };
};
type PayloadType = { modal: ModalName; state: ModalState };
const initialState: Record<ModalName, ModalState> = {
'auth.login': { open: false },
'auth.register': { open: false },
'auth.forgot': { open: false },
'auth.reset': { open: false },
'dashboard.create-resume': { open: false },
'dashboard.import-external': { open: false },
'dashboard.rename-resume': { open: false },
'builder.sections.profile': { open: false },
'builder.sections.work': { open: false },
'builder.sections.education': { open: false },
'builder.sections.awards': { open: false },
'builder.sections.certifications': { open: false },
'builder.sections.publications': { open: false },
'builder.sections.skills': { open: false },
'builder.sections.languages': { open: false },
'builder.sections.volunteer': { open: false },
'builder.sections.interests': { open: false },
'builder.sections.references': { open: false },
'builder.sections.projects': { open: false },
'builder.sections.custom': { open: false },
};
export const modalSlice = createSlice({
name: 'modal',
initialState,
reducers: {
setModalState: (state: Record<ModalName, ModalState>, action: PayloadAction<PayloadType>) => {
state[action.payload.modal] = action.payload.state;
},
},
});
export const { setModalState } = modalSlice.actions;
export default modalSlice.reducer;

View File

@ -0,0 +1,127 @@
import { ListItem, Profile, Resume, Section } from '@reactive-resume/schema';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import set from 'lodash/set';
import { v4 as uuidv4 } from 'uuid';
type SetResumeStatePayload = { path: string; value: unknown };
type AddItemPayload = { path: string; value: ListItem };
type EditItemPayload = { path: string; value: ListItem };
type DuplicateItemPayload = { path: string; value: ListItem };
type DeleteItemPayload = { path: string; value: ListItem };
type AddSectionPayload = { value: Section };
type DeleteSectionPayload = { path: string };
type DeletePagePayload = { page: number };
const initialState: Resume = {} as Resume;
export const resumeSlice = createSlice({
name: 'resume',
initialState,
reducers: {
setResume: (_state: Resume, action: PayloadAction<Resume>) => action.payload,
setResumeState: (state: Resume, action: PayloadAction<SetResumeStatePayload>) => {
const { path, value } = action.payload;
set(state, path, value);
},
addItem: (state: Resume, action: PayloadAction<AddItemPayload>) => {
const { path, value } = action.payload;
const id = uuidv4();
const list = get(state, path, []);
const item = merge(value, { id });
list.push(item);
set(state, path, list);
},
editItem: (state: Resume, action: PayloadAction<EditItemPayload>) => {
const { path, value } = action.payload;
const list: ListItem[] = get(state, path, []);
const index = list.findIndex((item) => item.id === value.id);
list[index] = value;
set(state, path, list);
},
duplicateItem: (state: Resume, action: PayloadAction<DuplicateItemPayload>) => {
const { path, value } = action.payload;
const list: ListItem[] = get(state, path, []);
const index = list.findIndex((item) => item.id === value.id);
const newItem = cloneDeep(list[index]);
newItem.id = uuidv4();
list.push(newItem);
set(state, path, list);
},
deleteItem: (state: Resume, action: PayloadAction<DeleteItemPayload>) => {
const { path, value } = action.payload;
let list = get(state, path, []);
list = list.filter((item: Profile) => item.id !== value.id);
set(state, path, list);
},
addSection: (state: Resume, action: PayloadAction<AddSectionPayload>) => {
const id = uuidv4();
const { value } = action.payload;
state.sections[id] = value;
state.metadata.layout[0][0].push(id);
},
deleteSection: (state: Resume, action: PayloadAction<DeleteSectionPayload>) => {
const { path } = action.payload;
const id = path && path.split('.').at(-1);
const sections = Object.keys(state.sections).filter((x) => x !== id);
const layout = state.metadata.layout.map((pages) => pages.map((list) => list.filter((x) => x !== id)));
set(state, 'sections', pick(state.sections, sections));
set(state, 'metadata.layout', layout);
},
addPage: (state: Resume) => {
state.metadata.layout.push([[], []]);
},
deletePage: (state: Resume, action: PayloadAction<DeletePagePayload>) => {
const { page } = action.payload;
// Do not delete the first page
if (page === 0) return;
// Get Sections defined in Page X
const [main, sidebar] = state.metadata.layout[page];
// Add sections to page 0 as a default
state.metadata.layout[0][0].push(...main);
state.metadata.layout[0][1].push(...sidebar);
state.metadata.layout.splice(page, 1);
},
},
});
export const {
setResume,
setResumeState,
addItem,
editItem,
duplicateItem,
deleteItem,
addSection,
deleteSection,
addPage,
deletePage,
} = resumeSlice.actions;
export default resumeSlice.reducer;

View File

@ -0,0 +1,34 @@
import { Resume } from '@reactive-resume/schema';
import debounce from 'lodash/debounce';
import { select, takeLatest } from 'redux-saga/effects';
import { updateResume } from '@/services/resume';
import {
addItem,
addSection,
deleteItem,
deleteSection,
duplicateItem,
editItem,
setResumeState,
} from '../resume/resumeSlice';
const DEBOUNCE_WAIT = 2500;
const debouncedSync = debounce((resume: Resume) => updateResume(resume), DEBOUNCE_WAIT);
function* handleSync() {
const resume: Resume = yield select((state) => state.resume);
debouncedSync(resume);
}
function* syncSaga() {
yield takeLatest(
[setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection],
handleSync
);
}
export default syncSaga;

17
client/store/storage.ts Normal file
View File

@ -0,0 +1,17 @@
import createWebStorage from 'redux-persist/lib/storage/createWebStorage';
const createNoopStorage = () => ({
getItem(_key: string) {
return Promise.resolve(null);
},
setItem(_key: string, value: string) {
return Promise.resolve(value);
},
removeItem(_key: string) {
return Promise.resolve();
},
});
const storage = typeof window !== 'undefined' ? createWebStorage('local') : createNoopStorage();
export default storage;