Skip to content
Merged
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,917 changes: 91 additions & 1,826 deletions app/components/SettingsView.tsx

Large diffs are not rendered by default.

815 changes: 815 additions & 0 deletions app/components/settings/AiSummariesSection.tsx

Large diffs are not rendered by default.

149 changes: 149 additions & 0 deletions app/components/settings/DeleteImportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

/**
* Confirm dialog for deleting an import from history.
*
* Two modes, chosen in the dialog itself: drop just the history row, or
* also remove the apps it added. The second is the destructive one, which
* is why the choice lives here rather than being implied by which button
* was clicked.
*/

import { useTranslations } from "next-intl";
import type { DateFormatMode } from "@/lib/date-format";
import type { DeleteTarget } from "@/lib/use-import-history";
import type { useModalFocus } from "@/lib/use-modal-focus";
import { fmtShortDate } from "./format";

export default function DeleteImportModal({
deleteTarget,
setDeleteTarget,
deleting,
confirmDeleteImport,
deleteImportModalRef,
dateMode,
}: {
/** null when closed; carries the import row and the chosen mode. */
deleteTarget: DeleteTarget | null;
setDeleteTarget: (next: DeleteTarget | null) => void;
deleting: boolean;
confirmDeleteImport: () => void;
deleteImportModalRef: ReturnType<typeof useModalFocus<HTMLDivElement>>;
dateMode: DateFormatMode;
}) {
const tModalDelete = useTranslations("settings.modals.delete_import");

return (
<>
{deleteTarget && (
<div
className="modal-overlay"
onClick={() => {
if (!deleting) {
setDeleteTarget(null);
}
}}
>
<div
aria-labelledby="delete-import-title"
aria-modal="true"
className="modal-card"
onClick={(event) => event.stopPropagation()}
ref={deleteImportModalRef}
role="dialog"
tabIndex={-1}
>
<div className="modal-badge">{tModalDelete("badge")}</div>
<h2 className="modal-title" id="delete-import-title">
{tModalDelete("title")}
</h2>
<p className="modal-copy">
{tModalDelete("meta", {
date: fmtShortDate(deleteTarget.importRow.createdAt, dateMode),
total: deleteTarget.importRow.total,
imported: deleteTarget.importRow.imported,
})}
</p>

<div className="delete-import-options">
<label
className={`delete-import-option${deleteTarget.mode === "history-only" ? " is-active" : ""}`}
>
<input
checked={deleteTarget.mode === "history-only"}
disabled={deleting}
name="delete-import-mode"
onChange={() =>
setDeleteTarget({ ...deleteTarget, mode: "history-only" })
}
type="radio"
value="history-only"
/>
<div>
<div className="delete-import-option-label">
{tModalDelete("option_history_only_label")}
</div>
<div className="delete-import-option-desc">
{tModalDelete("option_history_only_desc")}
</div>
</div>
</label>

<label
className={`delete-import-option${deleteTarget.mode === "with-apps" ? " is-active" : ""}`}
>
<input
checked={deleteTarget.mode === "with-apps"}
disabled={deleting}
name="delete-import-mode"
onChange={() =>
setDeleteTarget({ ...deleteTarget, mode: "with-apps" })
}
type="radio"
value="with-apps"
/>
<div>
<div className="delete-import-option-label">
{tModalDelete("option_with_apps_label")}
</div>
<div className="delete-import-option-desc">
{tModalDelete("option_with_apps_desc", {
count: deleteTarget.importRow.imported,
})}
</div>
</div>
</label>
</div>

<div className="modal-actions">
<button
className="btn btn-secondary"
disabled={deleting}
onClick={() => setDeleteTarget(null)}
type="button"
>
{tModalDelete("cancel")}
</button>
<button
className="btn btn-danger"
disabled={deleting}
onClick={() => void confirmDeleteImport()}
type="button"
>
{deleting ? (
<>
<span className="spinner-sm" /> {tModalDelete("deleting")}
</>
) : deleteTarget.mode === "with-apps" ? (
tModalDelete("confirm_with_apps")
) : (
tModalDelete("confirm_history")
)}
</button>
</div>
</div>
</div>
)}
</>
);
}
105 changes: 105 additions & 0 deletions app/components/settings/RemoveItemModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

/**
* Confirm dialog for the inline "Remove from Apps" button on an
* import-history row.
*
* Replaced a bare `window.confirm` so the destructive UX matches the
* wayback-remove and reset-app dialogs. The body is an IIFE because it
* derives a display label from the staged item before rendering.
*/

import { useTranslations } from "next-intl";
import type { PendingItemRemoval } from "@/lib/use-import-history";
import type { useModalFocus } from "@/lib/use-modal-focus";

export default function RemoveItemModal({
pendingItemRemoval,
setPendingItemRemoval,
removingItemId,
confirmRemoveItemFromDashboard,
removeItemModalRef,
}: {
/** null when closed; stages the import row, item and appId so the copy
* can name what is about to be removed. */
pendingItemRemoval: PendingItemRemoval | null;
setPendingItemRemoval: (next: PendingItemRemoval | null) => void;
/** Non-null while a removal is in flight — doubles as the busy flag. */
removingItemId: string | null;
confirmRemoveItemFromDashboard: () => void;
removeItemModalRef: ReturnType<typeof useModalFocus<HTMLDivElement>>;
}) {
const tModalRemoveApp = useTranslations("settings.modals.remove_app");

return (
<>
{/*
Confirm modal for the inline "Remove from Apps" button on an
import-history row. Replaces the previous `window.confirm` so
the destructive UX matches the wayback-remove + reset-app
dialogs above.
*/}
{pendingItemRemoval &&
(() => {
const { item } = pendingItemRemoval;
const label = item.appName || item.editedQuery || item.query;
const closing = removingItemId !== null;
return (
<div
className="modal-overlay"
onClick={() => {
if (!closing) {
setPendingItemRemoval(null);
}
}}
>
<div
aria-describedby="remove-item-copy"
aria-labelledby="remove-item-title"
aria-modal="true"
className="modal-card"
onClick={(event) => event.stopPropagation()}
ref={removeItemModalRef}
role="dialog"
tabIndex={-1}
>
<div className="modal-badge">{tModalRemoveApp("badge")}</div>
<h2 className="modal-title" id="remove-item-title">
{tModalRemoveApp("title", { name: label })}
</h2>
<p className="modal-copy" id="remove-item-copy">
{tModalRemoveApp("body")}
</p>
<div className="modal-actions">
<button
className="btn btn-secondary"
disabled={closing}
onClick={() => setPendingItemRemoval(null)}
type="button"
>
{tModalRemoveApp("cancel")}
</button>
<button
autoFocus
className="btn btn-danger"
disabled={closing}
onClick={() => void confirmRemoveItemFromDashboard()}
type="button"
>
{closing ? (
<>
<span aria-hidden="true" className="spinner-sm" />{" "}
{tModalRemoveApp("removing")}
</>
) : (
tModalRemoveApp("confirm")
)}
</button>
</div>
</div>
</div>
);
})()}
</>
);
}
120 changes: 120 additions & 0 deletions app/components/settings/ResetAppModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

/**
* Two-step confirm dialog for "Reset all data".
*
* Step 1 offers a backup export before anything is destroyed; step 2 is
* the actual confirmation. The export button is right there because the
* point at which someone is about to wipe the database is exactly when
* they should be offered a copy of it.
*/

import { useTranslations } from "next-intl";
import type { useModalFocus } from "@/lib/use-modal-focus";

export default function ResetAppModal({
resetStep,
setResetStep,
closeResetModal,
resetAllData,
resetting,
resetModalRef,
exportingBackup,
handleExportBackup,
}: {
/** 0 = closed, 1 = offer a backup, 2 = final confirmation. */
resetStep: 0 | 1 | 2;
setResetStep: (next: 0 | 1 | 2) => void;
closeResetModal: () => void;
resetAllData: () => void;
resetting: boolean;
resetModalRef: ReturnType<typeof useModalFocus<HTMLDivElement>>;
exportingBackup: boolean;
handleExportBackup: () => void;
}) {
const tBackupCard = useTranslations("settings.backup_card");
const tResetCard = useTranslations("settings.reset_app_card");

return (
<>
{resetStep > 0 && (
<div className="modal-overlay" onClick={closeResetModal}>
<div
aria-describedby="reset-app-copy"
aria-labelledby="reset-app-title"
aria-modal="true"
className="modal-card"
onClick={(event) => event.stopPropagation()}
ref={resetModalRef}
role="dialog"
tabIndex={-1}
>
<div className="modal-badge">{tResetCard("modal_badge")}</div>
<h2 className="modal-title" id="reset-app-title">
{resetStep === 1
? tResetCard("modal_title_step_1")
: tResetCard("modal_title_step_2")}
</h2>
<p className="modal-copy" id="reset-app-copy">
{resetStep === 1
? tResetCard("modal_body_step_1")
: tResetCard("modal_body_step_2")}
</p>
{resetStep === 2 && (
<div className="destructive-backup-offer">
<div className="destructive-backup-copy">
{tBackupCard("download_before_reset")}
</div>
<button
className="btn btn-secondary"
disabled={exportingBackup || resetting}
onClick={handleExportBackup}
type="button"
>
{exportingBackup
? tBackupCard("download_busy")
: tBackupCard("download_before_destructive")}
</button>
</div>
)}
<div className="modal-actions">
<button
className="btn btn-secondary"
disabled={resetting}
onClick={closeResetModal}
type="button"
>
{tResetCard("cancel")}
</button>

{resetStep === 1 ? (
<button
className="btn btn-danger"
onClick={() => setResetStep(2)}
type="button"
>
{tResetCard("continue")}
</button>
) : (
<button
className="btn btn-danger"
disabled={resetting}
onClick={() => void resetAllData()}
type="button"
>
{resetting ? (
<>
<span className="spinner-sm" /> {tResetCard("resetting")}
</>
) : (
tResetCard("delete_and_restart")
)}
</button>
)}
</div>
</div>
</div>
)}
</>
);
}
Loading
Loading