Skip to content

Commit f07d60c

Browse files
committed
Add local storage storage via saga
1 parent b37d4e6 commit f07d60c

File tree

7 files changed

+200
-23
lines changed

7 files changed

+200
-23
lines changed

photo-editor/src/App.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
2+
import { useDispatch } from 'react-redux';
23
import './App.css';
34
import PhotoUploader from './components/PhotoUploader';
45
import PhotoControls from './components/PhotoControls';
56
import ImageRenderer from './components/ImageRenderer';
7+
import { loadSavedPhoto } from './store/photo/actions';
8+
import { AppDispatch } from './store';
69

710
function App() {
11+
const dispatch = useDispatch<AppDispatch>();
12+
13+
useEffect(() => {
14+
// When the app loads, try to load any saved photo state
15+
dispatch(loadSavedPhoto());
16+
}, [dispatch]);
17+
818
return (
919
<div className="App">
1020
<header className="App-header">

photo-editor/src/components/PhotoUploader.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,28 @@ const PhotoUploader: React.FC = () => {
2020
if (file) {
2121
console.log('File selected:', file.name);
2222

23-
const fileUrl = URL.createObjectURL(file);
24-
25-
const img = new Image();
26-
img.onload = () => {
27-
const photo: Photo = {
28-
dimensions: {
29-
width: img.width,
30-
height: img.height
31-
},
32-
source: fileUrl,
33-
rotation: 0,
34-
filters: []
35-
};
23+
const reader = new FileReader();
24+
reader.onload = (e) => {
25+
const base64String = e.target?.result as string;
3626

37-
dispatch(setPhoto(photo));
27+
const img = new Image();
28+
img.onload = () => {
29+
const photo: Photo = {
30+
dimensions: {
31+
width: img.width,
32+
height: img.height
33+
},
34+
source: base64String,
35+
rotation: 0,
36+
filters: []
37+
};
38+
39+
dispatch(setPhoto(photo));
40+
};
41+
img.src = base64String;
3842
};
39-
img.src = fileUrl;
43+
44+
reader.readAsDataURL(file);
4045
}
4146
};
4247

photo-editor/src/store/photo/actions.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import {
55
SET_PHOTO,
66
ADD_FILTER,
77
REMOVE_FILTER,
8+
LOAD_SAVED_PHOTO,
9+
LOAD_SAVED_PHOTO_SUCCESS,
810
Photo,
911
PhotoFilter,
1012
ResetRotationAction,
1113
RotatePhotoAction,
1214
PanZoomPhotoAction,
1315
SetPhotoAction,
1416
AddFilterAction,
15-
RemoveFilterAction
17+
RemoveFilterAction,
18+
LoadSavedPhotoAction,
19+
LoadSavedPhotoSuccessAction
1620
} from './types';
1721

1822
// Action creators
@@ -55,3 +59,12 @@ export const removeFilter = (filter: PhotoFilter): RemoveFilterAction => ({
5559
filter
5660
}
5761
});
62+
63+
export const loadSavedPhoto = (): LoadSavedPhotoAction => ({
64+
type: LOAD_SAVED_PHOTO
65+
});
66+
67+
export const loadSavedPhotoSuccess = (photo: Photo): LoadSavedPhotoSuccessAction => ({
68+
type: LOAD_SAVED_PHOTO_SUCCESS,
69+
payload: photo
70+
});

photo-editor/src/store/photo/reducer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
PAN_ZOOM_PHOTO,
77
SET_PHOTO,
88
ADD_FILTER,
9-
REMOVE_FILTER
9+
REMOVE_FILTER,
10+
LOAD_SAVED_PHOTO_SUCCESS
1011
} from './types';
1112

1213
// Initial state
@@ -79,6 +80,12 @@ const photoReducer = (
7980
}
8081
};
8182

83+
case LOAD_SAVED_PHOTO_SUCCESS:
84+
return {
85+
...state,
86+
photo: action.payload
87+
};
88+
8289
default:
8390
return state;
8491
}

photo-editor/src/store/photo/sagas.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { takeLatest, debounce, put, select, call } from 'redux-saga/effects';
2+
import {
3+
SET_PHOTO,
4+
RESET_ROTATION,
5+
ROTATE_PHOTO,
6+
PAN_ZOOM_PHOTO,
7+
ADD_FILTER,
8+
REMOVE_FILTER,
9+
LOAD_SAVED_PHOTO,
10+
Photo
11+
} from './types';
12+
import { selectPhoto } from './selectors';
13+
import { loadSavedPhotoSuccess } from './actions';
14+
15+
const STORAGE_KEY = 'photo_editor_state';
16+
const IMAGE_STORAGE_KEY = 'photo_editor_image';
17+
const DEBOUNCE_TIME = 1000;
18+
const MAX_LOCAL_STORAGE_SIZE = 5 * 1024 * 1024;
19+
20+
const getFromLocalStorage = (key: string): string | null => {
21+
return localStorage.getItem(key);
22+
};
23+
24+
const setInLocalStorage = (key: string, value: string): void => {
25+
try {
26+
localStorage.setItem(key, value);
27+
} catch (e) {
28+
console.error(`Error storing data in localStorage (key: ${key}):`, e);
29+
throw e;
30+
}
31+
};
32+
33+
const removeFromLocalStorage = (key: string): void => {
34+
localStorage.removeItem(key);
35+
};
36+
37+
const willFitInLocalStorage = (str: string): boolean => {
38+
const estimatedSize = str.length * 2;
39+
return estimatedSize < MAX_LOCAL_STORAGE_SIZE;
40+
};
41+
42+
function* savePhotoState() {
43+
try {
44+
const photo: Photo | null = yield select(selectPhoto);
45+
46+
if (photo) {
47+
if (photo.source) {
48+
if (willFitInLocalStorage(photo.source)) {
49+
yield call(setInLocalStorage, IMAGE_STORAGE_KEY, photo.source);
50+
} else {
51+
console.warn('Image is too large for localStorage. Only saving settings.');
52+
}
53+
}
54+
55+
const storablePhoto = {
56+
...photo,
57+
source: 'has_stored_image'
58+
};
59+
60+
yield call(setInLocalStorage, STORAGE_KEY, JSON.stringify(storablePhoto));
61+
console.log('Photo state and image saved to localStorage');
62+
} else {
63+
yield call(removeFromLocalStorage, STORAGE_KEY);
64+
yield call(removeFromLocalStorage, IMAGE_STORAGE_KEY);
65+
console.log('Photo state removed from localStorage');
66+
}
67+
} catch (error) {
68+
console.error('Error saving photo state to localStorage:', error);
69+
}
70+
}
71+
72+
function* loadSavedPhotoState() {
73+
try {
74+
const savedPhotoJson: string | null = yield call(getFromLocalStorage, STORAGE_KEY);
75+
76+
if (savedPhotoJson) {
77+
const savedPhoto: Photo = JSON.parse(savedPhotoJson);
78+
79+
const imageSource: string | null = yield call(getFromLocalStorage, IMAGE_STORAGE_KEY);
80+
81+
if (imageSource) {
82+
const img = new Image();
83+
img.src = imageSource;
84+
85+
const imageLoads = new Promise<boolean>((resolve, reject) => {
86+
img.onload = () => resolve(true);
87+
img.onerror = () => reject(new Error('Failed to load saved image'));
88+
});
89+
90+
try {
91+
yield call(() => imageLoads);
92+
93+
savedPhoto.source = imageSource;
94+
95+
yield put(loadSavedPhotoSuccess(savedPhoto));
96+
console.log('Successfully restored saved photo with image');
97+
} catch (imgError) {
98+
console.error('Error loading saved image:', imgError);
99+
alert('Found saved photo settings, but the image could not be loaded. Please upload a new image.');
100+
}
101+
} else {
102+
alert('Found saved photo settings! Please upload an image to apply them.');
103+
console.log('Found saved photo settings in localStorage but no valid image source');
104+
}
105+
} else {
106+
console.log('No saved photo found in localStorage');
107+
}
108+
} catch (error) {
109+
console.error('Error loading photo state from localStorage:', error);
110+
}
111+
}
112+
113+
function* watchPhotoChanges() {
114+
yield takeLatest(SET_PHOTO, savePhotoState);
115+
116+
yield debounce(DEBOUNCE_TIME, [
117+
RESET_ROTATION,
118+
ROTATE_PHOTO,
119+
PAN_ZOOM_PHOTO,
120+
ADD_FILTER,
121+
REMOVE_FILTER
122+
], savePhotoState);
123+
124+
yield takeLatest(LOAD_SAVED_PHOTO, loadSavedPhotoState);
125+
}
126+
127+
export default function* photoSagas() {
128+
yield watchPhotoChanges();
129+
}

photo-editor/src/store/photo/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const PAN_ZOOM_PHOTO = 'photo/PAN_ZOOM_PHOTO';
2929
export const SET_PHOTO = 'photo/SET_PHOTO';
3030
export const ADD_FILTER = 'photo/ADD_FILTER';
3131
export const REMOVE_FILTER = 'photo/REMOVE_FILTER';
32+
export const LOAD_SAVED_PHOTO = 'photo/LOAD_SAVED_PHOTO';
33+
export const LOAD_SAVED_PHOTO_SUCCESS = 'photo/LOAD_SAVED_PHOTO_SUCCESS';
3234

3335
// Action interfaces
3436
export interface ResetRotationAction {
@@ -77,11 +79,24 @@ export interface RemoveFilterAction {
7779
[key: string]: any;
7880
}
7981

82+
export interface LoadSavedPhotoAction {
83+
type: typeof LOAD_SAVED_PHOTO;
84+
[key: string]: any;
85+
}
86+
87+
export interface LoadSavedPhotoSuccessAction {
88+
type: typeof LOAD_SAVED_PHOTO_SUCCESS;
89+
payload: Photo;
90+
[key: string]: any;
91+
}
92+
8093
// Union of all photo action types
8194
export type PhotoActionTypes =
8295
| ResetRotationAction
8396
| RotatePhotoAction
8497
| PanZoomPhotoAction
8598
| SetPhotoAction
8699
| AddFilterAction
87-
| RemoveFilterAction;
100+
| RemoveFilterAction
101+
| LoadSavedPhotoAction
102+
| LoadSavedPhotoSuccessAction;

photo-editor/src/store/rootSaga.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { all } from 'redux-saga/effects';
2-
// We'll import our photo sagas here later
3-
// import photoSagas from './photo/sagas';
2+
import photoSagas from './photo/sagas';
43

54
export default function* rootSaga() {
65
yield all([
7-
// We'll add our photo sagas here later
8-
// ...photoSagas,
6+
photoSagas(),
97
]);
108
}

0 commit comments

Comments
 (0)