mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-15 09:11:57 +10:00
🚀 release v3.0.0
This commit is contained in:
33
client/store/auth/authSlice.ts
Normal file
33
client/store/auth/authSlice.ts
Normal 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;
|
||||
70
client/store/build/buildSlice.ts
Normal file
70
client/store/build/buildSlice.ts
Normal 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
6
client/store/hooks.ts
Normal 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
44
client/store/index.ts
Normal 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;
|
||||
56
client/store/modal/modalSlice.ts
Normal file
56
client/store/modal/modalSlice.ts
Normal 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;
|
||||
127
client/store/resume/resumeSlice.ts
Normal file
127
client/store/resume/resumeSlice.ts
Normal 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;
|
||||
34
client/store/sagas/sync.ts
Normal file
34
client/store/sagas/sync.ts
Normal 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
17
client/store/storage.ts
Normal 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;
|
||||
Reference in New Issue
Block a user