chore: ran linting

This commit is contained in:
Anish Sarkar 2026-04-22 06:40:39 +05:30
parent 4a75603d4f
commit 3eb4d55ef5
17 changed files with 369 additions and 201 deletions

View file

@ -33,10 +33,7 @@ function toClientHeaders(headers: Headers) {
return nextHeaders;
}
async function proxy(
request: NextRequest,
context: { params: Promise<{ path?: string[] }> }
) {
async function proxy(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) {
const params = await context.params;
const path = params.path?.join("/") || "";
const upstreamUrl = new URL(`${getBackendBaseUrl()}/api/v1/${path}`);
@ -62,4 +59,12 @@ async function proxy(
});
}
export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE, proxy as OPTIONS, proxy as HEAD };
export {
proxy as GET,
proxy as POST,
proxy as PUT,
proxy as PATCH,
proxy as DELETE,
proxy as OPTIONS,
proxy as HEAD,
};

View file

@ -3,7 +3,7 @@
import { Check, Copy, Info } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useRef, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useApiKey } from "@/hooks/use-api-key";

View file

@ -200,8 +200,8 @@ export function DesktopContent() {
Launch on Startup
</CardTitle>
<CardDescription className="text-xs md:text-sm">
Automatically start SurfSense when you sign in to your computer so global
shortcuts and folder sync are always available.
Automatically start SurfSense when you sign in to your computer so global shortcuts and
folder sync are always available.
</CardDescription>
</CardHeader>
<CardContent className="px-3 md:px-6 pb-3 md:pb-6 space-y-3">
@ -232,8 +232,7 @@ export function DesktopContent() {
Start minimized to tray
</Label>
<p className="text-xs text-muted-foreground">
Skip the main window on boot SurfSense lives in the system tray until you need
it.
Skip the main window on boot SurfSense lives in the system tray until you need it.
</p>
</div>
<Switch

View file

@ -126,9 +126,7 @@ export function PurchaseHistoryContent() {
return [
...pagePurchases.map(normalizePagePurchase),
...tokenPurchases.map(normalizeTokenPurchase),
].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
}, [pagesQuery.data, tokensQuery.data]);
if (isLoading) {

View file

@ -4,17 +4,16 @@ import { Check, Copy, Info } from "lucide-react";
import { type FC, useCallback, useRef, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { useApiKey } from "@/hooks/use-api-key";
import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
const PLUGIN_RELEASES_URL =
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
const BACKEND_URL =
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://surfsense.com";
const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "https://surfsense.com";
/**
* Obsidian connect form for the plugin-only architecture.
@ -32,9 +31,7 @@ const BACKEND_URL =
export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
const [copiedUrl, setCopiedUrl] = useState(false);
const urlCopyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined
);
const urlCopyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const copyServerUrl = useCallback(async () => {
const ok = await copyToClipboardUtil(BACKEND_URL);
@ -59,9 +56,8 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<Info className="size-4 shrink-0 text-purple-500" />
<AlertTitle className="text-xs sm:text-sm">Plugin-based sync</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs">
SurfSense now syncs Obsidian via an official plugin that runs inside
Obsidian itself. Works on desktop and mobile, in cloud and self-hosted
deployments.
SurfSense now syncs Obsidian via an official plugin that runs inside Obsidian itself.
Works on desktop and mobile, in cloud and self-hosted deployments.
</AlertDescription>
</Alert>
@ -76,10 +72,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<h3 className="text-sm font-medium sm:text-base">Install the plugin</h3>
</header>
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
Grab the latest SurfSense plugin release. Once it's in the community
store, you'll also be able to install it from{" "}
<span className="font-medium">Settings Community plugins</span>{" "}
inside Obsidian.
Grab the latest SurfSense plugin release. Once it's in the community store, you'll
also be able to install it from{" "}
<span className="font-medium">Settings Community plugins</span> inside Obsidian.
</p>
<a
href={PLUGIN_RELEASES_URL}
@ -87,7 +82,12 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
rel="noopener noreferrer"
className="inline-flex"
>
<Button type="button" variant="secondary" size="sm" className="gap-2 text-xs sm:text-sm">
<Button
type="button"
variant="secondary"
size="sm"
className="gap-2 text-xs sm:text-sm"
>
Open plugin releases
</Button>
</a>
@ -104,9 +104,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<h3 className="text-sm font-medium sm:text-base">Copy your API key</h3>
</header>
<p className="mb-3 text-[11px] text-muted-foreground sm:text-xs">
Paste this into the plugin's <span className="font-medium">API token</span>{" "}
setting. The token expires after 24 hours. Long-lived personal access
tokens are coming in a future release.
Paste this into the plugin's <span className="font-medium">API token</span> setting.
The token expires after 24 hours. Long-lived personal access tokens are coming in a
future release.
</p>
{isLoading ? (
@ -151,9 +151,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<h3 className="text-sm font-medium sm:text-base">Point the plugin at this server</h3>
</header>
<p className="text-[11px] text-muted-foreground sm:text-xs">
For SurfSense Cloud, use the default <span className="font-medium">surfsense.com</span>.
If you are self-hosting, set the plugin's{" "}
<span className="font-medium">Server URL</span> to your frontend domain.
For SurfSense Cloud, use the default{" "}
<span className="font-medium">surfsense.com</span>. If you are self-hosting, set the
plugin's <span className="font-medium">Server URL</span> to your frontend domain.
</p>
</article>
@ -168,10 +168,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3>
</header>
<p className="text-[11px] text-muted-foreground sm:text-xs">
In the plugin's <span className="font-medium">Search space</span>{" "}
setting, choose the search space you want this vault to sync into.
The connector will appear here automatically once the plugin makes
its first sync.
In the plugin's <span className="font-medium">Search space</span> setting, choose the
search space you want this vault to sync into. The connector will appear here
automatically once the plugin makes its first sync.
</p>
</article>
</div>
@ -183,11 +182,9 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
What you get with Obsidian integration:
</h4>
<ul className="list-disc space-y-1 pl-5 text-[10px] text-muted-foreground sm:text-xs">
{getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR)?.map(
(benefit) => (
<li key={benefit}>{benefit}</li>
)
)}
{getConnectorBenefits(EnumConnectorName.OBSIDIAN_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>
)}

View file

@ -117,9 +117,7 @@ const PluginStats: FC<{ config: Record<string, unknown> }> = ({ config }) => {
label: "Files synced",
value:
placeholder ??
(typeof stats?.files_synced === "number"
? stats.files_synced.toLocaleString()
: "—"),
(typeof stats?.files_synced === "number" ? stats.files_synced.toLocaleString() : "—"),
},
];
}, [config.vault_name, stats, statsError]);
@ -139,10 +137,7 @@ const PluginStats: FC<{ config: Record<string, unknown> }> = ({ config }) => {
<h3 className="mb-3 text-sm font-medium sm:text-base">Vault status</h3>
<dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{tileRows.map((stat) => (
<div
key={stat.label}
className="rounded-lg bg-background/50 p-3"
>
<div key={stat.label} className="rounded-lg bg-background/50 p-3">
<dt className="text-xs tracking-wide text-muted-foreground sm:text-sm">
{stat.label}
</dt>
@ -160,8 +155,8 @@ const UnknownConnectorState: FC = () => (
<Info className="size-4 shrink-0" />
<AlertTitle className="text-xs sm:text-sm">Unrecognized config</AlertTitle>
<AlertDescription className="text-[11px] sm:text-xs">
This connector has neither plugin metadata nor a legacy marker. It may predate migration
you can safely delete it and re-install the SurfSense Obsidian plugin to resume syncing.
This connector has neither plugin metadata nor a legacy marker. It may predate migration you
can safely delete it and re-install the SurfSense Obsidian plugin to resume syncing.
</AlertDescription>
</Alert>
);

View file

@ -349,12 +349,7 @@ export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX
// `lib/posthog/events.ts` or per-connector tracking code.
// ============================================================================
export type ConnectorTelemetryGroup =
| "oauth"
| "composio"
| "crawler"
| "other"
| "unknown";
export type ConnectorTelemetryGroup = "oauth" | "composio" | "crawler" | "other" | "unknown";
export interface ConnectorTelemetryMeta {
connector_type: string;
@ -363,45 +358,44 @@ export interface ConnectorTelemetryMeta {
is_oauth: boolean;
}
const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap<string, ConnectorTelemetryMeta> =
(() => {
const map = new Map<string, ConnectorTelemetryMeta>();
const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap<string, ConnectorTelemetryMeta> = (() => {
const map = new Map<string, ConnectorTelemetryMeta>();
for (const c of OAUTH_CONNECTORS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "oauth",
is_oauth: true,
});
}
for (const c of COMPOSIO_CONNECTORS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "composio",
is_oauth: true,
});
}
for (const c of CRAWLERS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "crawler",
is_oauth: false,
});
}
for (const c of OTHER_CONNECTORS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "other",
is_oauth: false,
});
}
for (const c of OAUTH_CONNECTORS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "oauth",
is_oauth: true,
});
}
for (const c of COMPOSIO_CONNECTORS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "composio",
is_oauth: true,
});
}
for (const c of CRAWLERS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "crawler",
is_oauth: false,
});
}
for (const c of OTHER_CONNECTORS) {
map.set(c.connectorType, {
connector_type: c.connectorType,
connector_title: c.title,
connector_group: "other",
is_oauth: false,
});
}
return map;
})();
return map;
})();
/**
* Returns telemetry metadata for a connector_type, or a minimal "unknown"

View file

@ -350,11 +350,7 @@ export const useConnectorDialog = () => {
// Set connecting state immediately to disable button and show spinner
setConnectingId(connector.id);
trackConnectorSetupStarted(
Number(searchSpaceId),
connector.connectorType,
"oauth_click"
);
trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click");
try {
// Check if authEndpoint already has query parameters
@ -478,11 +474,7 @@ export const useConnectorDialog = () => {
(connectorType: string) => {
if (!searchSpaceId) return;
trackConnectorSetupStarted(
Number(searchSpaceId),
connectorType,
"non_oauth_click"
);
trackConnectorSetupStarted(Number(searchSpaceId), connectorType, "non_oauth_click");
setConnectingConnectorType(connectorType);
},

View file

@ -210,8 +210,7 @@ export function FreeChatPage() {
trackAnonymousChatMessageSent({
modelSlug,
messageLength: userQuery.trim().length,
hasUploadedDoc:
anonMode.isAnonymous && anonMode.uploadedDoc !== null ? true : false,
hasUploadedDoc: anonMode.isAnonymous && anonMode.uploadedDoc !== null ? true : false,
surface: "free_chat_page",
});

View file

@ -426,15 +426,50 @@ const AiSortIllustration = () => (
<title>AI File Sorting illustration showing automatic folder organization</title>
{/* Scattered documents on the left */}
<g opacity="0.5">
<rect x="20" y="40" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(-8 37 62)" />
<rect x="50" y="80" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(5 67 102)" />
<rect x="15" y="110" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(-3 32 132)" />
<rect
x="20"
y="40"
width="35"
height="45"
rx="4"
className="fill-neutral-200 dark:fill-neutral-700"
transform="rotate(-8 37 62)"
/>
<rect
x="50"
y="80"
width="35"
height="45"
rx="4"
className="fill-neutral-200 dark:fill-neutral-700"
transform="rotate(5 67 102)"
/>
<rect
x="15"
y="110"
width="35"
height="45"
rx="4"
className="fill-neutral-200 dark:fill-neutral-700"
transform="rotate(-3 32 132)"
/>
</g>
{/* AI sparkle / magic in the center */}
<g transform="translate(140, 90)">
<path d="M 0,-18 L 4,-6 L 16,-4 L 6,4 L 8,16 L 0,10 L -8,16 L -6,4 L -16,-4 L -4,-6 Z" className="fill-emerald-500 dark:fill-emerald-400" opacity="0.85">
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="10s" repeatCount="indefinite" />
<path
d="M 0,-18 L 4,-6 L 16,-4 L 6,4 L 8,16 L 0,10 L -8,16 L -6,4 L -16,-4 L -4,-6 Z"
className="fill-emerald-500 dark:fill-emerald-400"
opacity="0.85"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0"
to="360"
dur="10s"
repeatCount="indefinite"
/>
</path>
<circle cx="0" cy="0" r="3" className="fill-white dark:fill-emerald-200">
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
@ -442,51 +477,208 @@ const AiSortIllustration = () => (
</g>
{/* Animated sorting arrows */}
<g className="stroke-emerald-500 dark:stroke-emerald-400" strokeWidth="2" fill="none" opacity="0.6">
<g
className="stroke-emerald-500 dark:stroke-emerald-400"
strokeWidth="2"
fill="none"
opacity="0.6"
>
<path d="M 100 70 Q 140 60, 180 50" strokeDasharray="4,4">
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
<animate
attributeName="stroke-dashoffset"
from="8"
to="0"
dur="1s"
repeatCount="indefinite"
/>
</path>
<path d="M 100 100 Q 140 100, 180 100" strokeDasharray="4,4">
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
<animate
attributeName="stroke-dashoffset"
from="8"
to="0"
dur="1s"
repeatCount="indefinite"
/>
</path>
<path d="M 100 130 Q 140 140, 180 150" strokeDasharray="4,4">
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
<animate
attributeName="stroke-dashoffset"
from="8"
to="0"
dur="1s"
repeatCount="indefinite"
/>
</path>
</g>
{/* Organized folder tree on the right */}
{/* Root folder */}
<g>
<rect x="220" y="30" width="160" height="28" rx="6" className="fill-white dark:fill-neutral-800" opacity="0.9" />
<rect x="228" y="36" width="16" height="14" rx="3" className="fill-emerald-500 dark:fill-emerald-400" />
<line x1="252" y1="43" x2="330" y2="43" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2.5" strokeLinecap="round" />
<rect
x="220"
y="30"
width="160"
height="28"
rx="6"
className="fill-white dark:fill-neutral-800"
opacity="0.9"
/>
<rect
x="228"
y="36"
width="16"
height="14"
rx="3"
className="fill-emerald-500 dark:fill-emerald-400"
/>
<line
x1="252"
y1="43"
x2="330"
y2="43"
className="stroke-neutral-400 dark:stroke-neutral-500"
strokeWidth="2.5"
strokeLinecap="round"
/>
</g>
{/* Subfolder 1 */}
<g>
<line x1="240" y1="58" x2="240" y2="76" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
<line x1="240" y1="76" x2="250" y2="76" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
<rect x="250" y="64" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
<rect x="257" y="70" width="12" height="11" rx="2" className="fill-teal-400 dark:fill-teal-500" />
<line x1="276" y1="76" x2="340" y2="76" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
<line
x1="240"
y1="58"
x2="240"
y2="76"
className="stroke-neutral-300 dark:stroke-neutral-600"
strokeWidth="1.5"
/>
<line
x1="240"
y1="76"
x2="250"
y2="76"
className="stroke-neutral-300 dark:stroke-neutral-600"
strokeWidth="1.5"
/>
<rect
x="250"
y="64"
width="130"
height="24"
rx="5"
className="fill-white dark:fill-neutral-800"
opacity="0.85"
/>
<rect
x="257"
y="70"
width="12"
height="11"
rx="2"
className="fill-teal-400 dark:fill-teal-500"
/>
<line
x1="276"
y1="76"
x2="340"
y2="76"
className="stroke-neutral-400 dark:stroke-neutral-500"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
{/* Subfolder 2 */}
<g>
<line x1="240" y1="76" x2="240" y2="108" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
<line x1="240" y1="108" x2="250" y2="108" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
<rect x="250" y="96" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
<rect x="257" y="102" width="12" height="11" rx="2" className="fill-cyan-400 dark:fill-cyan-500" />
<line x1="276" y1="108" x2="350" y2="108" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
<line
x1="240"
y1="76"
x2="240"
y2="108"
className="stroke-neutral-300 dark:stroke-neutral-600"
strokeWidth="1.5"
/>
<line
x1="240"
y1="108"
x2="250"
y2="108"
className="stroke-neutral-300 dark:stroke-neutral-600"
strokeWidth="1.5"
/>
<rect
x="250"
y="96"
width="130"
height="24"
rx="5"
className="fill-white dark:fill-neutral-800"
opacity="0.85"
/>
<rect
x="257"
y="102"
width="12"
height="11"
rx="2"
className="fill-cyan-400 dark:fill-cyan-500"
/>
<line
x1="276"
y1="108"
x2="350"
y2="108"
className="stroke-neutral-400 dark:stroke-neutral-500"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
{/* Subfolder 3 */}
<g>
<line x1="240" y1="108" x2="240" y2="140" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
<line x1="240" y1="140" x2="250" y2="140" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
<rect x="250" y="128" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
<rect x="257" y="134" width="12" height="11" rx="2" className="fill-emerald-400 dark:fill-emerald-500" />
<line x1="276" y1="140" x2="325" y2="140" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
<line
x1="240"
y1="108"
x2="240"
y2="140"
className="stroke-neutral-300 dark:stroke-neutral-600"
strokeWidth="1.5"
/>
<line
x1="240"
y1="140"
x2="250"
y2="140"
className="stroke-neutral-300 dark:stroke-neutral-600"
strokeWidth="1.5"
/>
<rect
x="250"
y="128"
width="130"
height="24"
rx="5"
className="fill-white dark:fill-neutral-800"
opacity="0.85"
/>
<rect
x="257"
y="134"
width="12"
height="11"
rx="2"
className="fill-emerald-400 dark:fill-emerald-500"
/>
<line
x1="276"
y1="140"
x2="325"
y2="140"
className="stroke-neutral-400 dark:stroke-neutral-500"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
{/* Sparkle accents */}
@ -495,10 +687,22 @@ const AiSortIllustration = () => (
<animate attributeName="opacity" values="0;1;0" dur="2s" repeatCount="indefinite" />
</circle>
<circle cx="190" cy="155" r="1.5" className="fill-teal-400">
<animate attributeName="opacity" values="0;1;0" dur="2.5s" begin="0.8s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;0"
dur="2.5s"
begin="0.8s"
repeatCount="indefinite"
/>
</circle>
<circle cx="155" cy="120" r="1.5" className="fill-cyan-400">
<animate attributeName="opacity" values="0;1;0" dur="3s" begin="0.4s" repeatCount="indefinite" />
<animate
attributeName="opacity"
values="0;1;0"
dur="3s"
begin="0.4s"
repeatCount="indefinite"
/>
</circle>
</g>
</svg>

View file

@ -546,35 +546,35 @@ export function DocumentUploadTab({
</button>
)
) : (
<div
role="button"
tabIndex={0}
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent outline-none select-none"
onClick={() => {
if (!isElectron) fileInputRef.current?.click();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
<div
role="button"
tabIndex={0}
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent outline-none select-none"
onClick={() => {
if (!isElectron) fileInputRef.current?.click();
}
}}
>
<Upload className="h-10 w-10 text-muted-foreground" />
<div className="text-center space-y-1.5">
<p className="text-base font-medium">
{isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")}
</p>
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
</div>
<fieldset
className="w-full mt-1 border-none p-0 m-0"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (!isElectron) fileInputRef.current?.click();
}
}}
>
{renderBrowseButton({ fullWidth: true })}
</fieldset>
</div>
<Upload className="h-10 w-10 text-muted-foreground" />
<div className="text-center space-y-1.5">
<p className="text-base font-medium">
{isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")}
</p>
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
</div>
<fieldset
className="w-full mt-1 border-none p-0 m-0"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{renderBrowseButton({ fullWidth: true })}
</fieldset>
</div>
)}
</div>