Fork of atp.tools as a universal profile for people on the ATmosphere
1import { Input } from "@/components/ui/input";
2import { useState, useEffect } from "react";
3import { Search, X } from "lucide-react";
4import { useNavigate } from "@tanstack/react-router";
5import { KbdKey } from "./ui/kbdKey";
6import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
7import { Button } from "./ui/button";
8
9// function determineRouteType(input: string) {
10// if (input.startsWith("at://")) {
11// return "at url";
12// } else if (input.startsWith("https://")) {
13// return "pds";
14// } else {
15// return "handle";
16// }
17// return "unknown";
18// }
19
20export function isOnMac() {
21 return navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
22}
23
24function isMobile() {
25 return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
26 navigator.userAgent,
27 );
28}
29
30export function SmartSearchBar({
31 isKeybindEnabled = false,
32}: {
33 isKeybindEnabled?: boolean;
34}) {
35 const navigate = useNavigate();
36 const [input, setInput] = useState("");
37 const [open, setOpen] = useState(false);
38
39 const handleSubmit = (e: React.FormEvent) => {
40 e.preventDefault();
41 const value = input.trim();
42 if (value && value !== "@") {
43 // replace at:// with / to match the route correctly
44 if (value.includes("bsky.app") || value.includes("main.bsky.dev")) {
45 try {
46 const url = new URL(value);
47 const pathParts = url.pathname.split("/");
48
49 if (pathParts[1] === "profile") {
50 const handle = pathParts[2];
51
52 if (pathParts[3] === "post") {
53 // Post URL: /profile/handle/post/postid
54 navigate({
55 to: `/at:/${handle}/app.bsky.feed.post/${pathParts[4]}`,
56 });
57 } else if (pathParts[3] === "lists") {
58 // List URL: /profile/handle/lists/listid
59 navigate({
60 to: `/at:/${handle}/app.bsky.graph.list/${pathParts[4]}`,
61 });
62 } else {
63 // Profile URL: /profile/handle
64 navigate({
65 to: `/at:/${handle}`,
66 });
67 }
68 }
69 } catch (e) {
70 console.error("Invalid Bluesky URL:", e);
71 }
72 } else if (value.startsWith("pds/") || value.startsWith("https:/")) {
73 navigate({
74 to: "/pds/$url",
75 params: {
76 url: value.replace("https:/", "").replace("pds/", ""),
77 },
78 });
79 } else if (value === "typing") {
80 navigate({ to: "/rnfgrertt/typing" });
81 } else if (value === "pdsls") {
82 navigate({ to: "/rnfgrertt/pdsls" });
83 } else if (value === "borgle" || value === "wordle") {
84 navigate({ to: "/rnfgrertt/borgle" });
85 } else {
86 // Allow handle lookups to start with @
87 navigate({
88 to: `/at:/${value.replace("at:/", "").replace(/^@/, "")}`,
89 });
90 }
91 setOpen(false);
92 }
93 };
94
95 useEffect(() => {
96 if (isMobile()) return;
97 const handleKeyDown = (e: KeyboardEvent) => {
98 if (
99 (e.metaKey || e.ctrlKey) &&
100 e.key === "k" &&
101 isKeybindEnabled === true
102 ) {
103 e.preventDefault();
104 setOpen(true);
105 }
106 };
107
108 document.addEventListener("keydown", handleKeyDown);
109 return () => document.removeEventListener("keydown", handleKeyDown);
110 }, []);
111
112 return (
113 <Dialog open={open} onOpenChange={setOpen}>
114 <DialogTrigger asChild>
115 <Button
116 onClick={() => setOpen(true)}
117 variant="outline"
118 className="relative w-full justify-start text-muted-foreground"
119 >
120 <Search className="mr-2 h-4 w-4" />
121 <span className="flex-1 text-left">Search...</span>
122 {!isMobile() && (
123 <div className="md:flex items-center -mr-2 hidden">
124 <KbdKey keys={[isOnMac() ? "cmd" : "ctrl", "k"]} />
125 </div>
126 )}
127 </Button>
128 </DialogTrigger>
129
130 <DialogContent className="sm:max-w-[800px] p-0 border-0 rounded-full bg-transparent">
131 <form
132 onSubmit={handleSubmit}
133 className="relative backdrop-blur-3xl rounded-full"
134 >
135 <Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
136 <Input
137 type="text"
138 placeholder="Enter a handle, at url, or PDS url"
139 value={input}
140 onChange={(e) => setInput(e.currentTarget.value)}
141 className="border focus-visible:ring-0 focus-visible:ring-offset-0 pl-12 py-6 text-lg rounded-xl shadow-lg dark:shadow-slate-950"
142 autoCorrect="off"
143 autoCapitalize="none"
144 autoFocus
145 />
146 <div className="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-x-2">
147 {!isMobile() && <KbdKey keys={["esc"]} />}
148 <X
149 className="h-4 w-4 text-muted-foreground"
150 onClick={() => setOpen(false)}
151 />
152 </div>
153 </form>
154 </DialogContent>
155 </Dialog>
156 );
157}