Skip to content
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

Merged
merged 6 commits into from
Jun 1, 2024
Merged

Conversation

mickmister
Copy link
Member

@mickmister mickmister commented May 22, 2024

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. Like addCommentToSection
  • hooks - React hooks that are called by components
  • client - Functions called by actions that get/set persistent information. this layer can be swapped for a separate backend later
  • store - Functions that interact with localStorage directly. These LocalStorageStore store functions are called by the LocalStorageClient client functions

I 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

@mickmister mickmister requested a review from aelishRollo May 22, 2024 03:35
Copy link

netlify bot commented May 22, 2024

Deploy Preview for section-view ready!

Name Link
🔨 Latest commit a869535
🔍 Latest deploy log https://app.netlify.com/sites/section-view/deploys/664d785811cf6f00089b5ad6
😎 Deploy Preview https://deploy-preview-8--section-view.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

// const projectId = globalStore.getCurrentProjectId()
const projectId = 'project-1';

const addCommentToEntity = async (message: string, entityPointer: EntityPointer) => {
Copy link
Member Author

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'
}

Comment on lines +5 to +14
export enum EntityType {
PROJECT = 'project',
SECTION = 'section',
FILE = 'file',
}

export type EntityPointer = {
entityType: EntityType;
entityId: string;
}
Copy link
Member Author

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

Comment on lines +27 to +38
export type FileData = {
projectId: string;
id: string;
title: string;
} & EntityPointer;

export type CommentData = {
projectId: string;
id: string;
username: string;
message: string;
} & EntityPointer;
Copy link
Member Author

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

Comment on lines +3 to +5
export const matchesEntityPointer = (entityPointer: EntityPointer, entityType: EntityType, entityId: string): boolean => {
return entityPointer.entityType === entityType && entityPointer.entityId === entityId;
}
Copy link
Member Author

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

Copy link
Member

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?

Copy link
Member Author

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?

Comment on lines +10 to +16
const globalStore = useGlobalStore();

return (
<div className="files">
<span>+ Files</span>
{files.map((file) => {
const numComments = globalStore.getCommentsForFile(file.id).length;
Copy link
Member Author

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

Comment on lines +17 to +18
const projectId = 'project-1';
const sectionId = 'section-1';
Copy link
Member Author

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

Copy link
Member

@aelishRollo aelishRollo May 23, 2024

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

Comment on lines +12 to +31
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();
Copy link
Member Author

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

Comment on lines +40 to +51
clear = () => {
this.ls.clear();
this.currentData = this.migrateLocalStorageStore();
}

initializeWithSampleData = () => {
this.clear();
this.setAllProjects(initialProjects);
this.setAllSections(initialSections);
this.setAllFiles(initialFiles);
this.setAllComments(initialComments);
}
Copy link
Member Author

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

Copy link
Member

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

Copy link
Member Author

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

Copy link
Member Author

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

Comment on lines +3 to +20
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);
}
Copy link
Member Author

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

Comment on lines +9 to +30
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} />
Copy link
Member Author

@mickmister mickmister May 22, 2024

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

Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swag

@mickmister
Copy link
Member Author

mickmister commented May 22, 2024

@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

image

Five comments

image

@mickmister
Copy link
Member Author

mickmister commented May 22, 2024

@aelishRollo I recommend checking out this branch refactor-to-local-storage locally, and browsing the codebase through your IDE. This way you can see the current state of the files in your own environment without having to process diffs in GitHub's UI. You can also access things like Go to Definition that way too

I recommend first just looking at the component files, and see how they connect to the files that they import

Comment on lines +15 to +31
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>
Copy link
Member Author

@mickmister mickmister May 22, 2024

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

Copy link
Member

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?

Copy link
Member Author

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

@aelishRollo aelishRollo merged commit bc5af78 into main Jun 1, 2024
11 checks passed
@aelishRollo aelishRollo deleted the refactor-to-local-storage branch June 1, 2024 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants