Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
/public/
1 change: 1 addition & 0 deletions dist/main-1260c732d19e67143a06.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html><head><meta property="og:type" content="website"/><meta property="og:title" content="Hackweek Avatar Maker"/><meta property="og:description" content="Build customized avatars for Mozilla Hubs!"/><meta property="og:image" content="https://mozilla.github.io/hackweek-avatar-maker/assets/screenshot.png"/><title>Hackweek Avatar Maker</title></head><body><main><div id="root"></div></main><script src="dist/779-4ba5faaf41f8c7047d8b.js"></script><script src="dist/main-b6d3a598307a2456062e.js"></script></body></html>
<!doctype html><html><head><meta property="og:type" content="website"/><meta property="og:title" content="Hackweek Avatar Maker"/><meta property="og:description" content="Build customized avatars for Mozilla Hubs!"/><meta property="og:image" content="https://mozilla.github.io/hackweek-avatar-maker/assets/screenshot.png"/><title>Hackweek Avatar Maker</title></head><body><main><div id="root"></div></main><script src="dist/779-4ba5faaf41f8c7047d8b.js"></script><script src="dist/main-b6d3a598307a2456062e.js"></script></body></html>
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"start": "webpack serve",
"build": "NODE_ENV=production webpack",
"gen-assets": "node scripts/generate-assets.js",
"fix-assets": "node scripts/fix-assets.js"
"fix-assets": "node scripts/fix-assets.js",
"clean-public": "rm -r -f public",
"package-to-public": "npm run clean-public && mkdir public && cp -R dist/ public && cp -R assets public && cp index.html public/",
"build-for-netlify": "npm run build && npm run package-to-public"
},
"devDependencies": {
"@babel/core": "^7.12.10",
Expand Down
127 changes: 127 additions & 0 deletions src/persistence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useState, useEffect } from 'react'
import { simplePubSub } from "./utils/pub-sub";

const storageKey = "avatarMaker";

const slotKeyChange = simplePubSub();

function getConfigFromLocalStorage() {
const stored = localStorage.getItem(storageKey);
if (stored) {
return JSON.parse(stored);
} else {
return { avatars: {}, uiEnabled: false };
}
}

function persistToLocalStorage(config) {
localStorage.setItem(storageKey, JSON.stringify(config))
}

/**
* Saves an Avatar config object
* @param {string} slotKey key to store under
* @param {object} avatarConfig Avatar config object to store
*/
export function saveAvatarConfig(slotKey, avatarConfig) {
if (!window.localStorage) {
console.log("No localStorage...bailing.");
}

const storedConfig = getConfigFromLocalStorage();

storedConfig.avatars[slotKey] = avatarConfig;

persistToLocalStorage(storedConfig);
slotKeyChange.publish();
}

/**
* Returns the specified Avatar config object
* @param {string} slotKey The key for the desired Avatar config
* @returns {object} Avatar config
*/
export function getAvatarConfig(slotKey) {
if (!window.localStorage) {
console.log("No localStorage...bailing.");
return {};
}

const storedConfig = getConfigFromLocalStorage();

return storedConfig.avatars[slotKey];
}

/**
* Deletes the specified Avatar config object
* @param {string} slotKey The key for the desired Avatar config
*/
export function deleteAvatarConfig(slotKey) {
if (!window.localStorage) {
console.log("No localStorage...bailing.");
return;
}

const storedConfig = getConfigFromLocalStorage();

delete storedConfig.avatars[slotKey];
persistToLocalStorage(storedConfig);
slotKeyChange.publish();
}

/**
* Returns list of the Avatar slotKeys
* @returns {Array} List of Avatar slotKeys that have stored data.
*/
export function getAvatarConfigSlotKeys() {
if (!window.localStorage) {
console.log("No localStorage...bailing.");
return [];
}

const storedConfig = getConfigFromLocalStorage();

return Object.keys(storedConfig.avatars);
}

export function avatarPersistenceUIEnabled(newValue) {

const storedConfig = getConfigFromLocalStorage();

if (newValue !== undefined) {
storedConfig.uiEnabled = newValue;
persistToLocalStorage(storedConfig);
} else {
return storedConfig.uiEnabled || true;
}
}


// Expose the pieces that need to be reactive
export function usePersistenceUIEnabled() {
const [storedValue, setStoredValue] = useState(avatarPersistenceUIEnabled());

const setValue = (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
avatarPersistenceUIEnabled(valueToStore);
};

return [storedValue, setValue];
}

export function useAvatarConfigSlotKeys() {
const [slotKeys, setSlotKeys] = useState(getAvatarConfigSlotKeys);

useEffect(() => {
//Subscribe to changes
const subscriptionId = slotKeyChange
.subscribe(() => setSlotKeys(getAvatarConfigSlotKeys()));

return function cleanup() {
slotKeyChange.unsubscribe(subscriptionId);
}
});

return slotKeys;
}
6 changes: 5 additions & 1 deletion src/react-components/AvatarEditorContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { generateRandomConfig } from "../generate-random-config";
import initialAssets from "../assets";
import { isThumbnailMode } from "../utils";
import debounce from "../utils/debounce";
import { avatarPersistenceUIEnabled } from "../persistence";

// Used externally by the generate-thumbnails script
const thumbnailMode = isThumbnailMode();
Expand Down Expand Up @@ -109,6 +110,9 @@ export function AvatarEditorContainer() {
setAvatarConfig(generateRandomConfig(assets));
}

//Ideally this doesn't belong here. Is just here to allow the Saved Avatar display to be disabled.
window.avatarPersistenceUIEnabled = avatarPersistenceUIEnabled;

return (
<AvatarEditor
{...{
Expand Down Expand Up @@ -137,7 +141,7 @@ export function AvatarEditorContainer() {
),
rightPanel: <AvatarPreviewContainer {...{ thumbnailMode, canvasUrl }} />,
buttonTip: <ButtonTip {...tipState} />,
toolbar: <ToolbarContainer {...{ onGLBUploaded, randomizeConfig }} />,
toolbar: <ToolbarContainer {...{ onGLBUploaded, randomizeConfig, setAvatarConfig, avatarConfig }} />,
}}
/>
);
Expand Down
21 changes: 21 additions & 0 deletions src/react-components/AvatarPersistenceContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";
import { usePersistenceUIEnabled } from "../persistence";

import { AvatarPersistenceSaved } from "./AvatarPersistenceSaved";

export function AvatarPersistenceContainer({ setAvatarConfig, avatarConfig }) {
const [uiEnabled, setUIEnabled] = usePersistenceUIEnabled();

if (uiEnabled) {
return (
<div className="peristenceContainer">
<h2>Saved Avatars</h2>
<AvatarPersistenceSaved {...{ setAvatarConfig, avatarConfig }} />
</div>
)
} else {
return (
<></>
)
}
}
61 changes: 61 additions & 0 deletions src/react-components/AvatarPersistenceSaveAs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState } from "react";
import { faSave, faWindowClose } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { saveAvatarConfig } from "../persistence";

export function AvatarPersistenceSaveAs({ avatarConfig }) {
const [adding, setAdding] = useState(false);
const [name, setName] = useState('');

function openSaveAs() {
setAdding(true);
}

function cancel() {
setAdding(false);
setName('');
}

function cancelOnEscape(e) {
if (e.keyCode === 27) {
cancel();
}
}

function save() {
saveAvatarConfig(name, avatarConfig);
setAdding(false);
setName('');
}

function saveDisabled() {
return name === "";
}

if (adding) {
return (
<li className="saveAs open">
<div>
<input type="text" value={name} autoFocus
onChange={e => setName(e.target.value)}
onKeyDown={cancelOnEscape}
placeholder="Save Current Avatar as ..."/>
</div>
<div className="savedItemActions">
<button className="savedItemAction" title="Cancel" onClick={cancel}>
<FontAwesomeIcon icon={ faWindowClose } />
</button>
<button className="savedItemAction" title="Save" onClick={save} disabled={saveDisabled()} >
<FontAwesomeIcon icon={ faSave } />
</button>
</div>
</li>
)
} else {
return (
<li className="saveAs closed">
<button onClick={openSaveAs}>Save Current As...</button>
</li>
)
}
}
18 changes: 18 additions & 0 deletions src/react-components/AvatarPersistenceSaved.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import { useAvatarConfigSlotKeys } from "../persistence";
import { AvatarPersistenceSavedItem } from "./AvatarPersistenceSavedItem";
import { AvatarPersistenceSaveAs } from "./AvatarPersistenceSaveAs";

export function AvatarPersistenceSaved({ setAvatarConfig, avatarConfig }) {
const avatarConfigSlotKeys = useAvatarConfigSlotKeys();
const listItems = avatarConfigSlotKeys.map((key) => (
<AvatarPersistenceSavedItem slotKey={key} key={key} {...{ setAvatarConfig }} />
));

return (
<ul>
{listItems}
<AvatarPersistenceSaveAs {...{ avatarConfig }} />
</ul>
)
}
30 changes: 30 additions & 0 deletions src/react-components/AvatarPersistenceSavedItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useLayoutEffect } from "react";
import { faArrowAltCircleRight, faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { deleteAvatarConfig, getAvatarConfig } from "../persistence";

export function AvatarPersistenceSavedItem({ slotKey, setAvatarConfig }) {

function loadAvatar() {
setAvatarConfig(getAvatarConfig(slotKey));
}

function deleteAvatar() {
deleteAvatarConfig(slotKey);
}

return (
<li>
<div>{slotKey}</div>
<div className="savedItemActions">
<button className="savedItemAction" title="Delete Icon" onClick={deleteAvatar}>
<FontAwesomeIcon icon={ faTrashAlt } />
</button>
<button className="savedItemAction" title="Load Avatar" onClick={loadAvatar}>
<FontAwesomeIcon icon={ faArrowAltCircleRight } />
</button>
</div>
</li>
)

}
4 changes: 3 additions & 1 deletion src/react-components/ToolbarContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { Toolbar } from "./Toolbar";
import { UploadButton } from "./UploadButton";
import { MoreMenu } from "./MoreMenu";
import { AvatarPersistenceContainer } from "./AvatarPersistenceContainer";
import { dispatch } from "../dispatch";
import constants from "../constants";

Expand All @@ -13,14 +14,15 @@ function dispatchExportAvatar() {
dispatch(constants.exportAvatar);
}

export function ToolbarContainer({ onGLBUploaded, randomizeConfig }) {
export function ToolbarContainer({ onGLBUploaded, randomizeConfig, setAvatarConfig, avatarConfig }) {
return (
<Toolbar>
<span className="appName">Hackweek Avatar Maker</span>
<MoreMenu
items={
<>
<UploadButton onGLBUploaded={onGLBUploaded} />
<AvatarPersistenceContainer {...{ setAvatarConfig, avatarConfig }} />
<a href="https://github.com/mozilla/hackweek-avatar-maker" target="_blank">
GitHub
</a>
Expand Down
Loading