Add gallery submission (#218)
This adds a "Publish" UI to the creature and mandala pages which allows compositions to be published to the gallery.pull/223/head
rodzic
be3f1abab0
commit
ab3b976746
11
README.md
11
README.md
|
@ -89,9 +89,14 @@ to have the following configured:
|
||||||
// The gallery is globally readable.
|
// The gallery is globally readable.
|
||||||
allow read: if true;
|
allow read: if true;
|
||||||
|
|
||||||
// We don't yet support submitting to the gallery, so
|
allow write: if request.auth != null &&
|
||||||
// deny all writes for now.
|
request.resource.data.keys().hasOnly(['kind', 'serializedValue', 'owner', 'ownerName', 'title', 'createdAt']) &&
|
||||||
allow write: if false;
|
[request.resource.data.kind].hasAny(['creature', 'mandala']) &&
|
||||||
|
request.resource.data.serializedValue is string &&
|
||||||
|
request.resource.data.owner == request.auth.uid &&
|
||||||
|
request.resource.data.ownerName is string &&
|
||||||
|
request.resource.data.title is string &&
|
||||||
|
request.resource.data.createdAt is timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,9 @@ import React from "react";
|
||||||
export interface AuthContext {
|
export interface AuthContext {
|
||||||
/**
|
/**
|
||||||
* The currently logged-in user. This will be
|
* The currently logged-in user. This will be
|
||||||
* null if the user isn't logged in, otherwise it will
|
* null if the user isn't logged in.
|
||||||
* be their name.
|
|
||||||
*/
|
*/
|
||||||
loggedInUser: string | null;
|
loggedInUser: { name: string; id: string } | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the authentication provider, e.g. "GitHub",
|
* The name of the authentication provider, e.g. "GitHub",
|
||||||
|
|
|
@ -13,11 +13,19 @@ import {
|
||||||
getFirestore,
|
getFirestore,
|
||||||
collection,
|
collection,
|
||||||
getDocs,
|
getDocs,
|
||||||
|
query,
|
||||||
|
orderBy,
|
||||||
CollectionReference,
|
CollectionReference,
|
||||||
|
Timestamp,
|
||||||
|
addDoc,
|
||||||
} from "firebase/firestore";
|
} from "firebase/firestore";
|
||||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { AuthContext } from "./auth-context";
|
import { AuthContext } from "./auth-context";
|
||||||
import { GalleryComposition, GalleryContext } from "./gallery-context";
|
import {
|
||||||
|
GalleryComposition,
|
||||||
|
GalleryContext,
|
||||||
|
GallerySubmitStatus,
|
||||||
|
} from "./gallery-context";
|
||||||
|
|
||||||
const GALLERY_COLLECTION = "compositions";
|
const GALLERY_COLLECTION = "compositions";
|
||||||
|
|
||||||
|
@ -86,7 +94,10 @@ export const FirebaseGithubAuthProvider: React.FC<{}> = ({ children }) => {
|
||||||
}, [appCtx]);
|
}, [appCtx]);
|
||||||
|
|
||||||
const context: AuthContext = {
|
const context: AuthContext = {
|
||||||
loggedInUser: user && user.displayName,
|
loggedInUser: user && {
|
||||||
|
id: user.uid,
|
||||||
|
name: user.displayName || `GitHub user ${user.uid}`,
|
||||||
|
},
|
||||||
providerName: appCtx && "GitHub",
|
providerName: appCtx && "GitHub",
|
||||||
error,
|
error,
|
||||||
login: useCallback(() => {
|
login: useCallback(() => {
|
||||||
|
@ -103,7 +114,12 @@ export const FirebaseGithubAuthProvider: React.FC<{}> = ({ children }) => {
|
||||||
return <AuthContext.Provider value={context} children={children} />;
|
return <AuthContext.Provider value={context} children={children} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FirebaseCompositionDocument = Omit<GalleryComposition, "id">;
|
type FirebaseCompositionDocument = Omit<
|
||||||
|
GalleryComposition,
|
||||||
|
"id" | "createdAt"
|
||||||
|
> & {
|
||||||
|
createdAt: Timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
function getGalleryCollection(appCtx: FirebaseAppContext) {
|
function getGalleryCollection(appCtx: FirebaseAppContext) {
|
||||||
return collection(
|
return collection(
|
||||||
|
@ -112,40 +128,76 @@ function getGalleryCollection(appCtx: FirebaseAppContext) {
|
||||||
) as CollectionReference<FirebaseCompositionDocument>;
|
) as CollectionReference<FirebaseCompositionDocument>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function docToComp(
|
||||||
|
doc: FirebaseCompositionDocument,
|
||||||
|
id: string
|
||||||
|
): GalleryComposition {
|
||||||
|
const { createdAt, ...data } = doc;
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id,
|
||||||
|
createdAt: createdAt.toDate(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const FirebaseGalleryProvider: React.FC<{}> = ({ children }) => {
|
export const FirebaseGalleryProvider: React.FC<{}> = ({ children }) => {
|
||||||
const appCtx = useContext(FirebaseAppContext);
|
const appCtx = useContext(FirebaseAppContext);
|
||||||
const [compositions, setCompositions] = useState<GalleryComposition[]>([]);
|
const [compositions, setCompositions] = useState<GalleryComposition[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
const [lastRefresh, setLastRefresh] = useState(0);
|
const [lastRefresh, setLastRefresh] = useState(0);
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<GallerySubmitStatus>("idle");
|
||||||
const handleError = (e: Error) => {
|
const [lastSubmission, setLastSubmission] = useState<
|
||||||
setIsLoading(false);
|
GalleryComposition | undefined
|
||||||
setError(e.message);
|
>(undefined);
|
||||||
};
|
|
||||||
|
|
||||||
const context: GalleryContext = {
|
const context: GalleryContext = {
|
||||||
compositions,
|
compositions,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
lastRefresh,
|
lastRefresh,
|
||||||
|
lastSubmission,
|
||||||
|
submitStatus,
|
||||||
|
submit(props, onSuccess) {
|
||||||
|
if (!(appCtx && submitStatus === "idle")) return;
|
||||||
|
|
||||||
|
const doc: FirebaseCompositionDocument = {
|
||||||
|
...props,
|
||||||
|
createdAt: Timestamp.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setSubmitStatus("submitting");
|
||||||
|
setLastSubmission(undefined);
|
||||||
|
addDoc(getGalleryCollection(appCtx), doc)
|
||||||
|
.then((docRef) => {
|
||||||
|
const comp = docToComp(doc, docRef.id);
|
||||||
|
setSubmitStatus("idle");
|
||||||
|
setCompositions([comp, ...compositions]);
|
||||||
|
setLastSubmission(comp);
|
||||||
|
onSuccess(docRef.id);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setSubmitStatus("error");
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
refresh: useCallback(() => {
|
refresh: useCallback(() => {
|
||||||
if (!(appCtx && !isLoading)) return false;
|
if (!(appCtx && !isLoading)) return false;
|
||||||
|
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
getDocs(getGalleryCollection(appCtx))
|
getDocs(query(getGalleryCollection(appCtx), orderBy("createdAt", "desc")))
|
||||||
.then((snapshot) => {
|
.then((snapshot) => {
|
||||||
setLastRefresh(Date.now());
|
setLastRefresh(Date.now());
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setCompositions(
|
setCompositions(
|
||||||
snapshot.docs.map((doc) => ({
|
snapshot.docs.map((doc) => docToComp(doc.data(), doc.id))
|
||||||
id: doc.id,
|
|
||||||
...doc.data(),
|
|
||||||
}))
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(handleError);
|
.catch((e) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(e.message);
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}, [appCtx, isLoading]),
|
}, [appCtx, isLoading]),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
export type GalleryCompositionKind = "creature" | "mandala";
|
||||||
|
|
||||||
|
export type GallerySubmitStatus = "idle" | "submitting" | "error";
|
||||||
|
|
||||||
export type GalleryComposition = {
|
export type GalleryComposition = {
|
||||||
/** A unique identifier/primary key for the composition. */
|
/** A unique identifier/primary key for the composition. */
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
/** The type of composition. */
|
/** The type of composition. */
|
||||||
kind: "creature" | "mandala";
|
kind: GalleryCompositionKind;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The serialized value of the composition. This
|
* The serialized value of the composition. This
|
||||||
|
@ -24,6 +28,9 @@ export type GalleryComposition = {
|
||||||
|
|
||||||
/** The title of the composition. */
|
/** The title of the composition. */
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
/** When the composition was submitted to the gallery. */
|
||||||
|
createdAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,6 +43,25 @@ export interface GalleryContext {
|
||||||
*/
|
*/
|
||||||
compositions: GalleryComposition[];
|
compositions: GalleryComposition[];
|
||||||
|
|
||||||
|
/** The status of the most recent submission to the gallery. */
|
||||||
|
submitStatus: GallerySubmitStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a composition to the gallery. On success, calls the
|
||||||
|
* given callback, passing it the newly-assigned id of the
|
||||||
|
* composition.
|
||||||
|
*
|
||||||
|
* If already in the process of submitting a composition, this
|
||||||
|
* will do nothing.
|
||||||
|
*/
|
||||||
|
submit(
|
||||||
|
composition: Omit<GalleryComposition, "id" | "createdAt">,
|
||||||
|
onSuccess: (id: string) => void
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/** The most recent submission made via `submit()`, if any. */
|
||||||
|
lastSubmission?: GalleryComposition;
|
||||||
|
|
||||||
/** Whether we're currently loading the gallery from the network. */
|
/** Whether we're currently loading the gallery from the network. */
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
||||||
|
@ -62,5 +88,7 @@ export const GalleryContext = React.createContext<GalleryContext>({
|
||||||
compositions: [],
|
compositions: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
refresh: () => true,
|
refresh: () => true,
|
||||||
|
submitStatus: "idle",
|
||||||
|
submit: () => {},
|
||||||
lastRefresh: 0,
|
lastRefresh: 0,
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { assertNotNull } from "@justfixnyc/util/commonjs";
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { AuthContext } from "./auth-context";
|
||||||
|
import { GalleryCompositionKind, GalleryContext } from "./gallery-context";
|
||||||
|
|
||||||
|
export type GalleryWidgetProps = {
|
||||||
|
kind: GalleryCompositionKind;
|
||||||
|
serializeValue: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthWidget: React.FC<{}> = () => {
|
||||||
|
const ctx = useContext(AuthContext);
|
||||||
|
|
||||||
|
if (!ctx.providerName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = ctx.loggedInUser ? (
|
||||||
|
<button type="button" onClick={ctx.logout}>
|
||||||
|
Logout {ctx.loggedInUser.name}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={ctx.login}>
|
||||||
|
Login with {ctx.providerName}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const error = ctx.error ? <p className="error">{ctx.error}</p> : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{button}
|
||||||
|
{error}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginWidget: React.FC<{}> = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
To publish your composition to our gallery, you will first need to
|
||||||
|
login.
|
||||||
|
</p>
|
||||||
|
<AuthWidget />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PublishWidget: React.FC<GalleryWidgetProps> = (props) => {
|
||||||
|
const authCtx = useContext(AuthContext);
|
||||||
|
const user = assertNotNull(authCtx.loggedInUser, "User must be logged in");
|
||||||
|
const galleryCtx = useContext(GalleryContext);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [publishedId, setPublishedId] = useState("");
|
||||||
|
const handlePublish = () => {
|
||||||
|
galleryCtx.submit(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
kind: props.kind,
|
||||||
|
serializedValue: props.serializeValue(),
|
||||||
|
owner: user.id,
|
||||||
|
ownerName: user.name,
|
||||||
|
},
|
||||||
|
setPublishedId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const isSubmitting = galleryCtx.submitStatus === "submitting";
|
||||||
|
|
||||||
|
if (galleryCtx.lastSubmission?.id === publishedId) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Your composition "{title}" has been published!</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setPublishedId("");
|
||||||
|
setTitle("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
I want to publish more!
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Here you can publish your composition to our publicly-viewable gallery.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePublish();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-widget thingy">
|
||||||
|
<label htmlFor="gallery-title">Composition title:</label>
|
||||||
|
<input
|
||||||
|
id="gallery-title"
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={isSubmitting}>
|
||||||
|
Publish to gallery
|
||||||
|
</button>{" "}
|
||||||
|
{!isSubmitting && <AuthWidget />}
|
||||||
|
{galleryCtx.submitStatus === "error" && (
|
||||||
|
<p className="error">
|
||||||
|
Sorry, an error occurred while submitting your composition. Please
|
||||||
|
try again later.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GalleryWidget: React.FC<GalleryWidgetProps> = (props) => {
|
||||||
|
const authCtx = useContext(AuthContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<fieldset>
|
||||||
|
<legend>Publish</legend>
|
||||||
|
{authCtx.loggedInUser ? <PublishWidget {...props} /> : <LoginWidget />}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
};
|
|
@ -43,6 +43,8 @@ import { createDistribution } from "../../distribution";
|
||||||
import { ComponentWithShareableStateProps } from "../../page-with-shareable-state";
|
import { ComponentWithShareableStateProps } from "../../page-with-shareable-state";
|
||||||
import { useDebouncedEffect } from "../../use-debounced-effect";
|
import { useDebouncedEffect } from "../../use-debounced-effect";
|
||||||
import { useRememberedState } from "../../use-remembered-state";
|
import { useRememberedState } from "../../use-remembered-state";
|
||||||
|
import { GalleryWidget } from "../../gallery-widget";
|
||||||
|
import { serializeCreatureDesign } from "./serialization";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum number of attachment points that any symbol used as the main body
|
* The minimum number of attachment points that any symbol used as the main body
|
||||||
|
@ -344,6 +346,10 @@ export const CreaturePageWithDefaults: React.FC<
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</RandomizerWidget>
|
</RandomizerWidget>
|
||||||
|
<GalleryWidget
|
||||||
|
kind="creature"
|
||||||
|
serializeValue={() => serializeCreatureDesign(design)}
|
||||||
|
/>
|
||||||
<div className="thingy">
|
<div className="thingy">
|
||||||
<ExportWidget
|
<ExportWidget
|
||||||
basename={getDownloadBasename(creature.data.name)}
|
basename={getDownloadBasename(creature.data.name)}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { AuthContext } from "../auth-context";
|
|
||||||
import { AutoSizingSvg } from "../auto-sizing-svg";
|
import { AutoSizingSvg } from "../auto-sizing-svg";
|
||||||
import { CreatureContext, CreatureContextType } from "../creature-symbol";
|
import { CreatureContext, CreatureContextType } from "../creature-symbol";
|
||||||
import { createCreatureSymbolFactory } from "../creature-symbol-factory";
|
import { createCreatureSymbolFactory } from "../creature-symbol-factory";
|
||||||
|
@ -94,29 +93,6 @@ const RandomColorSampling: React.FC<{}> = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthWidget: React.FC<{}> = () => {
|
|
||||||
const ctx = useContext(AuthContext);
|
|
||||||
|
|
||||||
if (!ctx.providerName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const button = ctx.loggedInUser ? (
|
|
||||||
<button onClick={ctx.logout}>Logout {ctx.loggedInUser}</button>
|
|
||||||
) : (
|
|
||||||
<button onClick={ctx.login}>Login with {ctx.providerName}</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const error = ctx.error ? <p className="error">{ctx.error}</p> : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="thingy">
|
|
||||||
{button}
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DebugPage: React.FC<{}> = () => {
|
export const DebugPage: React.FC<{}> = () => {
|
||||||
const [symbolCtx, setSymbolCtx] = useState(createSvgSymbolContext());
|
const [symbolCtx, setSymbolCtx] = useState(createSvgSymbolContext());
|
||||||
const defaultCtx = useContext(CreatureContext);
|
const defaultCtx = useContext(CreatureContext);
|
||||||
|
@ -132,7 +108,6 @@ export const DebugPage: React.FC<{}> = () => {
|
||||||
<SymbolContextWidget ctx={symbolCtx} onChange={setSymbolCtx} />
|
<SymbolContextWidget ctx={symbolCtx} onChange={setSymbolCtx} />
|
||||||
<h2>Random color sampling</h2>
|
<h2>Random color sampling</h2>
|
||||||
<RandomColorSampling />
|
<RandomColorSampling />
|
||||||
<AuthWidget />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="canvas">
|
<div className="canvas">
|
||||||
<CreatureContext.Provider value={ctx}>
|
<CreatureContext.Provider value={ctx}>
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { MandalaCircle, MandalaCircleParams } from "../../mandala-circle";
|
||||||
import { useAnimationPct } from "../../animation";
|
import { useAnimationPct } from "../../animation";
|
||||||
import { RandomizerWidget } from "../../randomizer-widget";
|
import { RandomizerWidget } from "../../randomizer-widget";
|
||||||
import { useDebouncedEffect } from "../../use-debounced-effect";
|
import { useDebouncedEffect } from "../../use-debounced-effect";
|
||||||
|
import { GalleryWidget } from "../../gallery-widget";
|
||||||
|
import { serializeMandalaDesign } from "./serialization";
|
||||||
|
|
||||||
export type ExtendedMandalaCircleParams = MandalaCircleParams & {
|
export type ExtendedMandalaCircleParams = MandalaCircleParams & {
|
||||||
scaling: number;
|
scaling: number;
|
||||||
|
@ -398,6 +400,10 @@ export const MandalaPageWithDefaults: React.FC<{
|
||||||
setCircle2({ ...circle2, ...getRandomCircleParams(rng) });
|
setCircle2({ ...circle2, ...getRandomCircleParams(rng) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GalleryWidget
|
||||||
|
kind="mandala"
|
||||||
|
serializeValue={() => serializeMandalaDesign(design)}
|
||||||
|
/>
|
||||||
<div className="thingy">
|
<div className="thingy">
|
||||||
<ExportWidget
|
<ExportWidget
|
||||||
basename={getBasename(design)}
|
basename={getBasename(design)}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.13.13",
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/register": "^7.12.10",
|
"@babel/register": "^7.12.10",
|
||||||
|
"@justfixnyc/util": "^0.3.0",
|
||||||
"@types/cheerio": "^0.22.23",
|
"@types/cheerio": "^0.22.23",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^14.14.22",
|
||||||
|
@ -2904,6 +2905,11 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@justfixnyc/util": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@justfixnyc/util/-/util-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-Iuj6x5PPhtaHFNLYfZoIOFIwV4ysPGt7EapgfO8hYlQprHRcZfxBXb51ZHmWx3I7Ak5VZLgOLc8/HLFkZjWKlw=="
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
@ -13468,6 +13474,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@justfixnyc/util": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@justfixnyc/util/-/util-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-Iuj6x5PPhtaHFNLYfZoIOFIwV4ysPGt7EapgfO8hYlQprHRcZfxBXb51ZHmWx3I7Ak5VZLgOLc8/HLFkZjWKlw=="
|
||||||
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.13.13",
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/register": "^7.12.10",
|
"@babel/register": "^7.12.10",
|
||||||
|
"@justfixnyc/util": "^0.3.0",
|
||||||
"@types/cheerio": "^0.22.23",
|
"@types/cheerio": "^0.22.23",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^14.14.22",
|
||||||
|
|
Ładowanie…
Reference in New Issue