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
152 changes: 152 additions & 0 deletions apps/extension/src/entrypoints/pages/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Download, Upload } from "lucide-solid";
import { createSignal, Show } from "solid-js";
import type { BangAliases, RankingsV2 } from "@/utils/storage";
import {
bangAliasesData,
items,
quickBangsData,
syncedRankings,
} from "@/utils/storage";

interface SyncSettingsBackup {
version: 1;
exportedAt: string;
data: {
rankings: RankingsV2;
quick_bangs: string[];
bang_aliases: BangAliases;
};
}

const Settings = () => {
const [importStatus, setImportStatus] = createSignal<{
type: "success" | "error";
message: string;
} | null>(null);

const exportSettings = () => {
const backup: SyncSettingsBackup = {
version: 1,
exportedAt: new Date().toISOString(),
data: {
rankings: syncedRankings() ?? {},
quick_bangs: quickBangsData() ?? [],
bang_aliases: bangAliasesData() ?? {},
},
};

const blob = new Blob([JSON.stringify(backup, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `searchtuner-backup-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};

const importSettings = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;

try {
const text = await file.text();
const backup = JSON.parse(text) as SyncSettingsBackup;

// Validate backup structure
if (
!backup.version ||
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Version check is too loose - accepts any truthy version number instead of specifically validating version 1.

Suggested change
!backup.version ||
backup.version !== 1 ||
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/extension/src/entrypoints/pages/components/Settings.tsx
Line: 65:65

Comment:
**logic:** Version check is too loose - accepts any truthy version number instead of specifically validating version 1.

```suggestion
					backup.version !== 1 ||
```

How can I resolve this? If you propose a fix, please make it concise.

!backup.data ||
typeof backup.data !== "object"
) {
throw new Error("Invalid backup file format");
}

// Import each setting if present
if (backup.data.rankings && typeof backup.data.rankings === "object") {
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: typeof === "object" also returns true for arrays, allowing [] to be imported as rankings which should be an object.

Suggested change
if (backup.data.rankings && typeof backup.data.rankings === "object") {
if (backup.data.rankings && typeof backup.data.rankings === "object" && !Array.isArray(backup.data.rankings)) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/extension/src/entrypoints/pages/components/Settings.tsx
Line: 73:73

Comment:
**logic:** `typeof === "object"` also returns true for arrays, allowing `[]` to be imported as rankings which should be an object.

```suggestion
				if (backup.data.rankings && typeof backup.data.rankings === "object" && !Array.isArray(backup.data.rankings)) {
```

How can I resolve this? If you propose a fix, please make it concise.

await items.rankings.setValue(backup.data.rankings);
}

if (Array.isArray(backup.data.quick_bangs)) {
await items.quick_bangs.setValue(backup.data.quick_bangs);
}

if (
backup.data.bang_aliases &&
typeof backup.data.bang_aliases === "object"
Comment on lines +81 to +83
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: typeof === "object" also returns true for arrays, allowing [] to be imported as bang_aliases which should be an object.

Suggested change
if (
backup.data.bang_aliases &&
typeof backup.data.bang_aliases === "object"
if (
backup.data.bang_aliases &&
typeof backup.data.bang_aliases === "object" &&
!Array.isArray(backup.data.bang_aliases)
) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/extension/src/entrypoints/pages/components/Settings.tsx
Line: 81:83

Comment:
**logic:** `typeof === "object"` also returns true for arrays, allowing `[]` to be imported as bang_aliases which should be an object.

```suggestion
				if (
					backup.data.bang_aliases &&
					typeof backup.data.bang_aliases === "object" &&
					!Array.isArray(backup.data.bang_aliases)
				) {
```

How can I resolve this? If you propose a fix, please make it concise.

) {
await items.bang_aliases.setValue(backup.data.bang_aliases);
}

setImportStatus({
type: "success",
message: "Settings imported successfully!",
});

// Clear status after 3 seconds
setTimeout(() => setImportStatus(null), 3000);
} catch (err) {
setImportStatus({
type: "error",
message:
err instanceof Error ? err.message : "Failed to import settings",
});

// Clear status after 5 seconds
setTimeout(() => setImportStatus(null), 5000);
}
};
input.click();
};

return (
<div class="mx-auto max-w-2xl">
<div class="rounded-lg border border-foreground/20 p-4">
<h3 class="mb-2 font-semibold text-lg">Backup & Restore</h3>
<p class="mb-4 text-foreground/70 text-sm">
Export your synced settings to a file or import from a previous
backup. This includes your domain rankings, quick bangs, and bang
aliases.
</p>

<div class="flex gap-3">
<button
onClick={exportSettings}
class="flex items-center gap-2 rounded bg-foreground/10 px-4 py-2 font-medium text-sm hover:bg-foreground/20"
>
<Download size={16} />
Export Settings
</button>
<button
onClick={() => void importSettings()}
class="flex items-center gap-2 rounded bg-foreground/10 px-4 py-2 font-medium text-sm hover:bg-foreground/20"
>
<Upload size={16} />
Import Settings
</button>
</div>

<Show when={importStatus()}>
{(status) => (
<p
class={`mt-4 text-sm ${
status().type === "success" ? "text-green-500" : "text-red-500"
}`}
>
{status().message}
</p>
)}
</Show>
</div>
</div>
);
};

export default Settings;
15 changes: 9 additions & 6 deletions apps/extension/src/entrypoints/pages/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { A, HashRouter, Route, useNavigate } from "@solidjs/router";
import { ArrowUpDown, Hash, Info, Settings } from "lucide-solid";
import {
ArrowUpDown,
Hash,
Info,
Settings as SettingsIcon,
} from "lucide-solid";
import { render } from "solid-js/web";
import logo from "@/assets/icon.webp";
import { RankingsTable } from "@/entrypoints/pages/components/rankingsTable";
import About from "./components/About";
import Bangs from "./components/Bangs";
import Settings from "./components/Settings";

function App() {
return (
Expand All @@ -18,7 +24,7 @@ function App() {
Rankings
</A>
<A href="/settings" class="flex items-center gap-2">
<Settings />
<SettingsIcon />
Settings
</A>
<A href="/bangs" class="flex items-center gap-2">
Expand All @@ -44,10 +50,7 @@ function App() {
return <></>;
}}
/>
<Route
path="/settings"
component={() => <div class="text-2xl">Settings</div>}
/>
<Route path="/settings" component={Settings} />
<Route path="/rankings" component={RankingsTable} />
<Route path="/about" component={About} />
<Route path="/bangs" component={Bangs} />
Expand Down