diff --git a/src/components/ui/SearchableMultiSelect.tsx b/src/components/ui/SearchableMultiSelect.tsx
new file mode 100644
index 0000000..6730e13
--- /dev/null
+++ b/src/components/ui/SearchableMultiSelect.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+
+export type SearchableOption = {
+ id: number;
+ label: string;
+ depth?: number;
+};
+
+type SearchableMultiSelectProps = {
+ options: SearchableOption[];
+ value: number[];
+ onChange: (next: number[]) => void;
+ placeholder?: string;
+ searchPlaceholder?: string;
+ emptyLabel?: string;
+ disabled?: boolean;
+};
+
+export default function SearchableMultiSelect({
+ options,
+ value,
+ onChange,
+ placeholder = "انتخاب کنید",
+ searchPlaceholder = "جستجو...",
+ emptyLabel = "موردی یافت نشد",
+ disabled = false,
+}: SearchableMultiSelectProps) {
+ const rootRef = useRef
(null);
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState("");
+
+ const selected = useMemo(() => {
+ const map = new Map(options.map((o) => [o.id, o]));
+ return value.map((id) => map.get(id)).filter(Boolean) as SearchableOption[];
+ }, [options, value]);
+
+ const filtered = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ if (!q) return options;
+ return options.filter((o) => o.label.toLowerCase().includes(q));
+ }, [options, query]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onDoc = (e: MouseEvent) => {
+ if (!rootRef.current?.contains(e.target as Node)) {
+ setOpen(false);
+ setQuery("");
+ }
+ };
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ setOpen(false);
+ setQuery("");
+ }
+ };
+ document.addEventListener("mousedown", onDoc);
+ window.addEventListener("keydown", onKey);
+ return () => {
+ document.removeEventListener("mousedown", onDoc);
+ window.removeEventListener("keydown", onKey);
+ };
+ }, [open]);
+
+ function toggle(id: number) {
+ if (value.includes(id)) onChange(value.filter((x) => x !== id));
+ else onChange([...value, id]);
+ }
+
+ function remove(id: number) {
+ onChange(value.filter((x) => x !== id));
+ }
+
+ return (
+
+
+
+ {open ? (
+
+
+ setQuery(e.target.value)}
+ placeholder={searchPlaceholder}
+ className="h-9 w-full rounded border border-line bg-white px-3 text-sm outline-none focus:border-ink"
+ />
+
+
+ {filtered.length === 0 ? (
+ -
+ {emptyLabel}
+
+ ) : (
+ filtered.map((opt) => {
+ const checked = value.includes(opt.id);
+ return (
+ -
+
+
+ );
+ })
+ )}
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/lib/auth-handoff.ts b/src/lib/auth-handoff.ts
new file mode 100644
index 0000000..7dc5213
--- /dev/null
+++ b/src/lib/auth-handoff.ts
@@ -0,0 +1,42 @@
+/**
+ * After auth on the store app, send the user back to an external page
+ * (e.g. website /partners) with tokens in the URL hash for cross-origin handoff.
+ * Returns true if a handoff redirect was started.
+ */
+export function redirectWithAuthHandoff(returnTo: string | null | undefined): boolean {
+ if (typeof window === "undefined" || !returnTo) return false;
+
+ try {
+ const target = new URL(returnTo);
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
+ return false;
+ }
+
+ // Same-origin return — no token handoff needed.
+ if (target.origin === window.location.origin) {
+ window.location.href = target.toString();
+ return true;
+ }
+
+ const access = localStorage.getItem("mana_access_token");
+ const refresh = localStorage.getItem("mana_refresh_token");
+ const user = localStorage.getItem("mana_user");
+ if (!access || !refresh) return false;
+
+ const hash = new URLSearchParams({
+ access_token: access,
+ refresh_token: refresh,
+ ...(user ? { user } : {}),
+ });
+ target.hash = hash.toString();
+ window.location.href = target.toString();
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function readReturnToParam(): string | null {
+ if (typeof window === "undefined") return null;
+ return new URLSearchParams(window.location.search).get("returnTo");
+}
diff --git a/src/lib/partnership-api.ts b/src/lib/partnership-api.ts
new file mode 100644
index 0000000..3061fa7
--- /dev/null
+++ b/src/lib/partnership-api.ts
@@ -0,0 +1,99 @@
+import { API_URL, ApiError, api } from "@/lib/api";
+
+const STORE_SLUG =
+ process.env.NEXT_PUBLIC_STORE_SLUG?.trim() || "mana-collection";
+
+export type PublicCategoryNode = {
+ id: number;
+ parentId: number | null;
+ nameFa: string;
+ nameEn: string;
+ children: PublicCategoryNode[];
+};
+
+export type FlatCategoryOption = {
+ id: number;
+ label: string;
+ depth: number;
+};
+
+export type CreatePartnershipInput = {
+ businessNameFa: string;
+ businessNameEn?: string;
+ categoryIds: number[];
+ description: string;
+ imageUrls: string[];
+ landline?: string;
+ instagram?: string;
+ city: string;
+ province: string;
+ address: string;
+ postalCode: string;
+};
+
+function flattenCategories(
+ nodes: PublicCategoryNode[],
+ depth = 0,
+): FlatCategoryOption[] {
+ const out: FlatCategoryOption[] = [];
+ for (const node of nodes) {
+ out.push({
+ id: node.id,
+ label: node.nameFa || node.nameEn || String(node.id),
+ depth,
+ });
+ if (node.children?.length) {
+ out.push(...flattenCategories(node.children, depth + 1));
+ }
+ }
+ return out;
+}
+
+export async function fetchPublicCategoryOptions(): Promise<
+ FlatCategoryOption[]
+> {
+ const data = await api<{ categories: PublicCategoryNode[] }>(
+ `/public/categories?storeSlug=${encodeURIComponent(STORE_SLUG)}`,
+ { auth: false },
+ );
+ return flattenCategories(data.categories ?? []);
+}
+
+export async function createPartnershipRequest(body: CreatePartnershipInput) {
+ return api("/partnership-requests", { method: "POST", body });
+}
+
+export async function uploadPartnershipImage(file: File): Promise<{
+ url: string;
+}> {
+ const form = new FormData();
+ form.append("file", file);
+
+ const headers: Record = {};
+ const token =
+ typeof window !== "undefined"
+ ? localStorage.getItem("mana_access_token")
+ : null;
+ if (token) headers.Authorization = `Bearer ${token}`;
+
+ const res = await fetch(`${API_URL}/partnership-requests/images`, {
+ method: "POST",
+ headers,
+ body: form,
+ });
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => null);
+ const message =
+ (body && typeof body === "object" && "message" in body
+ ? Array.isArray((body as { message: unknown }).message)
+ ? (body as { message: string[] }).message.join("، ")
+ : String((body as { message: unknown }).message)
+ : null) ||
+ res.statusText ||
+ "بارگذاری تصویر ناموفق بود";
+ throw new ApiError(res.status, message, body);
+ }
+
+ return (await res.json()) as { url: string };
+}