-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor to use local storage #8
Conversation
✅ Deploy Preview for section-view ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
src/actions/useActions.ts
Outdated
// const projectId = globalStore.getCurrentProjectId() | ||
const projectId = 'project-1'; | ||
|
||
const addCommentToEntity = async (message: string, entityPointer: EntityPointer) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here entityPointer
is essentially the thing that the comment is going to be attached to. At the moment, comments can technically be attached to the following:
- project
- section
- file
So if this comment is going to be added to project-1
, then entityPointer
would have this value:
{
entityType: 'project',
entityId: 'project-1'
}
export enum EntityType { | ||
PROJECT = 'project', | ||
SECTION = 'section', | ||
FILE = 'file', | ||
} | ||
|
||
export type EntityPointer = { | ||
entityType: EntityType; | ||
entityId: string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we define the EntityPointer
type, which is used to attach objects to other objects
export type FileData = { | ||
projectId: string; | ||
id: string; | ||
title: string; | ||
} & EntityPointer; | ||
|
||
export type CommentData = { | ||
projectId: string; | ||
id: string; | ||
username: string; | ||
message: string; | ||
} & EntityPointer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Things that can be attached to other things will have this & EntityPointer
thing in its type
. I think this reads well. If you think it would read better, we can inline the entityType
and entityId
fields on this type
instead
export const matchesEntityPointer = (entityPointer: EntityPointer, entityType: EntityType, entityId: string): boolean => { | ||
return entityPointer.entityType === entityType && entityPointer.entityId === entityId; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is only for making code that calls this shorter. Not sure how much value it brings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm more concerned with how readable something is, both in terms of brevity and in terms of how clear it is what the code is doing, without any comments. Essentially using variable names such that the code itself is almost like reading comments.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm in full agreement. So how do you feel about that in the context of this code we're commenting on?
const globalStore = useGlobalStore(); | ||
|
||
return ( | ||
<div className="files"> | ||
<span>+ Files</span> | ||
{files.map((file) => { | ||
const numComments = globalStore.getCommentsForFile(file.id).length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useGlobalStore
hook helps get arbitrary things from the "already loaded" data. All data for the current project will be loaded at all times, so this data will always be available
const projectId = 'project-1'; | ||
const sectionId = 'section-1'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These will eventually be taken from the URL parameters instead of being hardcoded here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love it. Nice and extendable with clear intentions
const LOCAL_STORAGE_KEY_PROJECTS = 'projects'; | ||
const LOCAL_STORAGE_KEY_SECTIONS = 'sections'; | ||
const LOCAL_STORAGE_KEY_FILES = 'files'; | ||
const LOCAL_STORAGE_KEY_COMMENTS = 'comments'; | ||
|
||
export type StoreData = { | ||
projects: ProjectData[]; | ||
sections: SectionData[]; | ||
files: FileData[]; | ||
comments: CommentData[]; | ||
} | ||
|
||
export class LocalStorageStore { | ||
private ls: LocalStorageDependency; | ||
|
||
private currentData: StoreData; | ||
|
||
constructor(ls: LocalStorageDependency) { | ||
this.ls = ls; | ||
this.currentData = this.migrateLocalStorageStore(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file should only need to be edited if we add more types of data to the store
clear = () => { | ||
this.ls.clear(); | ||
this.currentData = this.migrateLocalStorageStore(); | ||
} | ||
|
||
initializeWithSampleData = () => { | ||
this.clear(); | ||
this.setAllProjects(initialProjects); | ||
this.setAllSections(initialSections); | ||
this.setAllFiles(initialFiles); | ||
this.setAllComments(initialComments); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking we can have two buttons on the UI:
- "Clear data" - Calls
store.clear()
and reloads the page - "Reset with sample data" - Calls
store.initializeWithSampleData()
and reloads the page
The reloading of the page allows the application to re-initialize from local storage appropriately
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting. I'd love to know more about your thought process for having those be the two buttons. Especially from a data flow perspective
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're just for debugging really. Those buttons would not be present in the production version of the application. The data flow is "update local storage then restart the program through reloading the page". That's the least obtrusive way to put that debug functionality in there I think. No special case code that's like "seed this data then re-read from local storage" anywhere in the code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand your question fully though
export class MockLocalStorageDependency<T extends Record<string, object>> implements LocalStorageDependency { | ||
public currentData: T; | ||
|
||
constructor(data: T) { | ||
this.currentData = data; | ||
} | ||
|
||
getItem(key: string): string | null { | ||
if (this.currentData[key]) { | ||
return JSON.stringify(this.currentData[key]); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
setItem(key: string, value: string): void { | ||
this.currentData[key as keyof T] = JSON.parse(value); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file won't need to change unless we want to call more methods on localStorage
. It essentially mocks localStorage
itself, at least the subset of its methods being used by the application
import {SectionTitle} from './SectionTitle'; | ||
import {useGlobalStore} from './hooks/useGlobalStore'; | ||
|
||
type SectionPageProps = { | ||
projectId: string; | ||
sectionId: string; | ||
} | ||
|
||
const SectionPage: React.FC<SectionPageProps> = ({projectId, sectionId}) => { | ||
const globalStore = useGlobalStore(); | ||
const section = globalStore.getSection(sectionId); | ||
const files = globalStore.getFilesForSection(sectionId); | ||
|
||
const sectionPointer: types.EntityPointer = { | ||
entityId: sectionId, | ||
entityType: types.EntityType.SECTION, | ||
}; | ||
|
||
return ( | ||
<div className="root"> | ||
<SectionTitle sectionId={sectionId} /> | ||
<ChordProgression chordProgression={section.chordProgression} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made some whitespace changes in the PR. Please let me know what you think about:
- 4 spaces indent instead of 2 spaces - I personally feel strongly about this. It helps me scan the code much quicker and efficiently this way. The one argument someone may have against it is that it can make the code look too indented. My argument against that is that could be a sign that you need to address the number of levels of indentation happening on that line of code
- Named imports have no spacings inside the braces - I feel less strongly about this, but still prefer it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I think four spaces is a great standard. Greatly enhances readability, which always goes a long way, even if that's not a measurable thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swag
@aelishRollo I've found that when you add more comments to the page, elements towards the top of the page start vertically shrinking. This is present in the css code on the main branch One comment Five comments |
@aelishRollo I recommend checking out this branch I recommend first just looking at the component files, and see how they connect to the files that they import |
const globalStore = useGlobalStore(); | ||
const section = globalStore.getSection(sectionId); | ||
const files = globalStore.getFilesForSection(sectionId); | ||
|
||
const sectionPointer: types.EntityPointer = { | ||
entityId: sectionId, | ||
entityType: types.EntityType.SECTION, | ||
}; | ||
|
||
return ( | ||
<div className="root"> | ||
<SectionTitle sectionId={sectionId} /> | ||
<ChordProgression chordProgression={section.chordProgression} /> | ||
<Files files={files} /> | ||
<Comments entityPointer={sectionPointer} /> | ||
<CreateComment entityPointer={sectionPointer} /> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A better design of how props would be passed in here may be like this:
const SectionPage: React.FC<SectionPageProps> = ({projectId, sectionId}) => {
const globalStore = useGlobalStore();
const actions = useActions();
const section = globalStore.getSection(sectionId);
const files = globalStore.getFilesForSection(sectionId);
const comments = globalStore.getCommentsForSection(sectionId);
const sectionPointer: types.EntityPointer = {
entityId: sectionId,
entityType: types.EntityType.SECTION,
};
const updateTitle = (newTitle: string) => {
actions.updateSection(...);
};
const uploadFileToSection = (fileContent: File) => {
actions.uploadFileToSection(sectionId, fileContent);
};
const addCommentToSection = (message: string) => {
actions.addCommentToSection(sectionId, message);
};
return (
<div className="root">
<SectionTitle
title={section.title}
onEdit={updateTitle}
/>
<ChordProgression
chordProgression={section.chordProgression}
/>
<Files
files={files}
uploadFile={uploadFileToSection}
/>
<Comments
comments={comments}
/>
<CreateComment
addComment={addCommentToSection}
/>
</div>
);
};
Then the child components can be as "dumb" as possible https://devdojo.com/krissanawat101/minimal-explanation-of-dumb-and-smart-components-in-react
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The frontend is currently quite non-elastic, and will definitely need to be refactored in a lot of ways to make it more robust for different viewports, as well as supporting the functionality we're looking to implement.
I think having a centralized way of managing all of these will go a long. Maybe we just make issues for each little issue like this, and then I can go through and clean them all up. What do you think is the best way to systematically tackle all of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think itemizing things is the way to go. I'll spend some time with that soon
This PR adapts the application to use local storage to persist user-generated data.
A few directories have been added to the project:
actions
- Functions that are called by components for user actions. LikeaddCommentToSection
hooks
- React hooks that are called by componentsclient
- Functions called byactions
that get/set persistent information. this layer can be swapped for a separate backend laterstore
- Functions that interact withlocalStorage
directly. TheseLocalStorageStore
store
functions are called by theLocalStorageClient
client
functionsI also want to move the components to a
components
directory as well, but I wanted to make the diff between the component files obvious so I kept the same locations for the component files in this PR.https://deploy-preview-8--section-view.netlify.app