+
}
+ data-tooltip="Import a previously downloaded zip file of edits"
+ onClick={(event) =>
+ (
+ (event.currentTarget as HTMLButtonElement)
+ .nextElementSibling as HTMLInputElement
+ )?.click()
+ }
+ />
+
{
+ /** get uploaded file object */
+ const file = (event.target?.files || [])[0];
+ if (file) importData(await uploadZip(await file.arrayBuffer()));
+ /** reset input */
+ if (uploadRef.current) uploadRef.current.value = "";
+ }}
+ />
+
}
- data-tooltip="Download your edits as a backup or to submit a pull request manually."
+ data-tooltip="Download your edits as a backup or to submit a pull request manually"
onClick={() => downloadZip(exportData(), `${lesson} ${language}`)}
/>
diff --git a/app/src/pages/edit/Header.tsx b/app/src/pages/edit/Header.tsx
index f63f51078..b43de0a34 100644
--- a/app/src/pages/edit/Header.tsx
+++ b/app/src/pages/edit/Header.tsx
@@ -6,6 +6,7 @@ import {
FaHouse,
FaLock,
FaRegCircleQuestion,
+ FaUpload,
} from "react-icons/fa6";
import { useParams } from "react-router";
import { Link } from "react-router-dom";
diff --git a/app/src/pages/edit/Row.tsx b/app/src/pages/edit/Row.tsx
index 6fb8ce384..257c87412 100644
--- a/app/src/pages/edit/Row.tsx
+++ b/app/src/pages/edit/Row.tsx
@@ -8,11 +8,12 @@ import {
FaStop,
FaThumbsUp,
} from "react-icons/fa6";
+import { LuBraces } from "react-icons/lu";
import { Link, useParams } from "react-router-dom";
import classNames from "classnames";
import { useAtom, useAtomValue } from "jotai";
-import { cloneDeep } from "lodash";
-import { issueLink } from "@/api";
+import { cloneDeep, truncate } from "lodash";
+import { issueLink, repoFull } from "@/api";
import { playing, playSegment, stopVideo, time } from "@/components/Player";
import Textarea from "@/components/Textarea";
import { isEdited, originalMax, translationMax } from "@/data/data";
@@ -21,6 +22,7 @@ import {
captions,
completion,
description,
+ meta,
showLegacy,
title,
} from "@/pages/Edit";
@@ -33,10 +35,11 @@ import classes from "./Row.module.css";
type Props = {
index: number;
entries: typeof title | typeof captions | typeof description;
+ file: string;
};
/** editable entry row */
-function Row({ index, entries }: Props) {
+function Row({ index, entries, file }: Props) {
/** get/set entry from list of entries and index */
const [getEntries, setEntries] = useAtom(entries);
const entry = getEntries[index]!;
@@ -64,6 +67,7 @@ function Row({ index, entries }: Props) {
const { lesson = "", language = "" } = useParams();
/** page state */
+ const getMeta = useAtomValue(meta);
const getTime = useAtomValue(time);
const getPlaying = useAtomValue(playing);
const getShowLegacy = useAtomValue(showLegacy);
@@ -89,15 +93,29 @@ function Row({ index, entries }: Props) {
/** right to left languages need special styling */
const rtlLanguage = isRtl(language);
+ /** get (rough) line number in raw json */
+ const lineNumber = 1 + index * (6 + 2) + 2;
+ /** get first few words in original english, url-encoded */
+ const firstFewWords = window.encodeURIComponent(
+ startingOriginal.split(/\s+/).slice(0, 20).join(" "),
+ );
+
/** issue url params */
const issueTitle = `${lesson}/${language}`;
const issueBody = [
- `**Line**:\n`,
- `${startingOriginal.slice(0, 100)}...\n`,
- "\n",
- ...(start && end ? ["**Time**:\n", `${start} - ${end}\n`, "\n"] : []),
- `**Describe the issue**:\n`,
- ];
+ ["**Original**", truncate(startingOriginal, { length: 300 })],
+ ["**Translation**:", truncate(startingTranslation, { length: 300 })],
+ start && end ? ["**Time**:", `${start} - ${end}`] : [],
+ ["**Line**", `Entry # ${index}`, `Line # ~${lineNumber}`],
+ ["**Describe the issue**"],
+ ]
+ .filter((line) => line.length)
+ .map((line) => line.join("\n"))
+ .join("\n\n")
+ .concat("\n");
+
+ /** get full path to file and line number */
+ const path = `${repoFull}/tree/main/${getMeta?.path}/${language}/${file}#:~:text=${firstFewWords}`;
return (
+
+
+
+
diff --git a/app/src/pages/edit/Section.tsx b/app/src/pages/edit/Section.tsx
index fa5240583..ab1e21cdb 100644
--- a/app/src/pages/edit/Section.tsx
+++ b/app/src/pages/edit/Section.tsx
@@ -16,10 +16,11 @@ import classes from "../Edit.module.css";
type Props = {
label: string;
entries: typeof title | typeof captions | typeof description;
+ file: string;
};
/** title/caption/description editor rows */
-function Section({ label, entries }: Props) {
+function Section({ label, entries, file }: Props) {
/** url params */
const { lesson = "", language = "" } = useParams();
@@ -36,7 +37,7 @@ function Section({ label, entries }: Props) {
{getEntries.map((entry, index) =>
filterFuncs[getFilter](entry) ? (
-
+
) : (
),
diff --git a/app/src/pages/home/Search.tsx b/app/src/pages/home/Search.tsx
index b198ce58f..b4ccfac7a 100644
--- a/app/src/pages/home/Search.tsx
+++ b/app/src/pages/home/Search.tsx
@@ -27,7 +27,9 @@ function Search() {
const filtered = flatLessons.filter(
({ lesson, title, topic, language }) =>
nameTerms.every(
- (term) => title.includes(term) || lesson.toLowerCase().includes(term),
+ (term) =>
+ title.toLowerCase().includes(term) ||
+ lesson.toLowerCase().includes(term),
) &&
(topic === "" || topic.toLowerCase().includes(topicSearch)) &&
language.toLowerCase().includes(languageSearch),
diff --git a/app/src/util/download.ts b/app/src/util/download.ts
index 9c18b5125..f25dca9b6 100644
--- a/app/src/util/download.ts
+++ b/app/src/util/download.ts
@@ -1,24 +1,60 @@
-import { BlobWriter, TextReader, ZipWriter } from "@zip.js/zip.js";
+import {
+ BlobReader,
+ BlobWriter,
+ TextReader,
+ TextWriter,
+ ZipReader,
+ ZipWriter,
+} from "@zip.js/zip.js";
/** download zip of json objects */
export async function downloadZip(
jsons: Record,
filename: string,
) {
- const zipWriter = new ZipWriter(new BlobWriter("application/zip"));
- await Promise.all(
- Object.entries(jsons).map(([key, json]) =>
- zipWriter.add(
- key + ".json",
- new TextReader(JSON.stringify(json, null, 2)),
+ try {
+ const zipWriter = new ZipWriter(new BlobWriter("application/zip"));
+ await Promise.all(
+ Object.entries(jsons).map(([key, json]) =>
+ zipWriter.add(
+ key + ".json",
+ new TextReader(JSON.stringify(json, null, 2)),
+ ),
),
- ),
- );
- const blob = await zipWriter.close();
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = filename + ".zip";
- link.click();
- window.URL.revokeObjectURL(url);
+ );
+ const blob = await zipWriter.close();
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = filename + ".zip";
+ link.click();
+ window.URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error(error);
+ window.alert("Error downloading file");
+ }
}
+
+/** upload zip of json objects */
+export async function uploadZip(data: ArrayBuffer) {
+ try {
+ const zipReader = new ZipReader(new BlobReader(new Blob([data])));
+ const entries = await zipReader.getEntries();
+ const files: UploadResult = {};
+ await Promise.all(
+ entries.map(async (entry) => {
+ const text = (await entry.getData?.(new TextWriter())) || "[]";
+ files[entry.filename] = JSON.parse(text);
+ }),
+ );
+ await zipReader.close();
+ return files;
+ } catch (error) {
+ console.error(error);
+ window.alert("Error uploading file");
+ }
+
+ return {};
+}
+
+export type UploadResult = Record;