Skip to content

Commit bcc6bdc

Browse files
added share modal
1 parent ddce339 commit bcc6bdc

File tree

1 file changed

+256
-3
lines changed
  • apps/desktop2/src/components/main/body/sessions/outer-header

1 file changed

+256
-3
lines changed
Lines changed: 256 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,262 @@
1+
import { Avatar, AvatarFallback } from "@hypr/ui/components/ui/avatar";
12
import { Button } from "@hypr/ui/components/ui/button";
3+
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
4+
import {
5+
Select,
6+
SelectContent,
7+
SelectItem,
8+
SelectTrigger,
9+
SelectValue,
10+
} from "@hypr/ui/components/ui/select";
11+
import { Separator } from "@hypr/ui/components/ui/separator";
12+
13+
import { CircleMinus, Link2Icon, SearchIcon } from "lucide-react";
14+
import { useState } from "react";
15+
16+
interface Person {
17+
id: string;
18+
name: string;
19+
email?: string;
20+
role: "viewer" | "editor";
21+
isParticipant?: boolean;
22+
}
223

324
export function ShareButton(_: { sessionId: string }) {
25+
const [searchQuery, setSearchQuery] = useState("");
26+
const [selectedPeople, setSelectedPeople] = useState<Person[]>([]);
27+
const [invitedPeople, setInvitedPeople] = useState<Person[]>([
28+
{ id: "1", name: "John Doe", email: "john@example.com", role: "editor", isParticipant: true },
29+
{ id: "2", name: "Jane Smith", email: "jane@example.com", role: "editor", isParticipant: true },
30+
]);
31+
32+
const searchResults: Person[] = searchQuery.trim()
33+
? [
34+
{ id: "3", name: "Alice Johnson", email: "alice@example.com", role: "viewer" as const },
35+
{ id: "4", name: "Bob Wilson", email: "bob@example.com", role: "viewer" as const },
36+
].filter((person) => person.name.toLowerCase().includes(searchQuery.toLowerCase()))
37+
: [];
38+
39+
const handleSelectPerson = (person: Person) => {
40+
if (!selectedPeople.find((p) => p.id === person.id)) {
41+
setSelectedPeople([...selectedPeople, person]);
42+
}
43+
setSearchQuery("");
44+
};
45+
46+
const handleRemoveSelected = (personId: string) => {
47+
setSelectedPeople(selectedPeople.filter((p) => p.id !== personId));
48+
};
49+
50+
const handleInvite = () => {
51+
const newInvites = selectedPeople.filter(
52+
(selected) => !invitedPeople.find((invited) => invited.id === selected.id),
53+
);
54+
setInvitedPeople([...invitedPeople, ...newInvites]);
55+
setSelectedPeople([]);
56+
// TODO: Implement actual invite functionality
57+
console.log("Invite:", newInvites);
58+
};
59+
60+
const handleRemovePerson = (personId: string) => {
61+
setInvitedPeople(invitedPeople.filter((p) => p.id !== personId));
62+
};
63+
64+
const handleRoleChange = (personId: string, role: "viewer" | "editor") => {
65+
setInvitedPeople(
66+
invitedPeople.map((p) => (p.id === personId ? { ...p, role } : p)),
67+
);
68+
};
69+
70+
const handleCopyLink = () => {
71+
// TODO: Implement copy link functionality
72+
console.log("Copy link");
73+
};
74+
75+
const getInitials = (name: string) => {
76+
return name
77+
.split(" ")
78+
.map((n) => n[0])
79+
.join("")
80+
.toUpperCase()
81+
.slice(0, 2);
82+
};
83+
484
return (
5-
<Button size="sm" variant="ghost">
6-
Share
7-
</Button>
85+
<Popover>
86+
<PopoverTrigger asChild>
87+
<Button size="sm" variant="ghost">
88+
Share
89+
</Button>
90+
</PopoverTrigger>
91+
92+
<PopoverContent className="w-[360px] shadow-lg" align="end">
93+
<div className="flex flex-col gap-4">
94+
<div className="flex flex-col gap-2">
95+
<div className="flex items-center px-3 py-2 gap-2 rounded-md bg-neutral-50 border border-neutral-200">
96+
<SearchIcon className="size-4 text-neutral-700 flex-shrink-0" />
97+
<input
98+
type="text"
99+
value={searchQuery}
100+
onChange={(e) => setSearchQuery(e.target.value)}
101+
placeholder="Search for people"
102+
className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-500"
103+
/>
104+
</div>
105+
106+
{searchQuery.trim() && searchResults.length > 0 && (
107+
<div className="flex flex-col rounded-md border border-neutral-200 overflow-hidden">
108+
{searchResults.map((person) => (
109+
<button
110+
key={person.id}
111+
type="button"
112+
onClick={() => handleSelectPerson(person)}
113+
className="flex items-center justify-between px-3 py-2 text-sm text-left hover:bg-neutral-100 transition-colors w-full disabled:opacity-50"
114+
disabled={
115+
selectedPeople.some((p) => p.id === person.id)
116+
|| invitedPeople.some((p) => p.id === person.id)
117+
}
118+
>
119+
<div className="flex items-center gap-2">
120+
<Avatar className="size-6">
121+
<AvatarFallback className="text-xs bg-neutral-200 text-neutral-700 font-medium">
122+
{getInitials(person.name)}
123+
</AvatarFallback>
124+
</Avatar>
125+
<div className="flex flex-col">
126+
<span className="font-medium truncate">{person.name}</span>
127+
{person.email && (
128+
<span className="text-xs text-neutral-500">{person.email}</span>
129+
)}
130+
</div>
131+
</div>
132+
</button>
133+
))}
134+
</div>
135+
)}
136+
</div>
137+
138+
{selectedPeople.length > 0 && (
139+
<div className="flex flex-col gap-2">
140+
<div className="flex flex-col rounded-md border border-neutral-100 bg-neutral-50 overflow-hidden">
141+
{selectedPeople.map((person) => (
142+
<div
143+
key={person.id}
144+
className="flex items-center justify-between gap-3 py-2 px-3 hover:bg-neutral-100 group transition-colors"
145+
>
146+
<div className="flex items-center gap-2.5 relative min-w-0 flex-1">
147+
<div className="relative size-7 flex items-center justify-center flex-shrink-0">
148+
<div className="absolute inset-0 flex items-center justify-center transition-opacity group-hover:opacity-0">
149+
<Avatar className="size-7">
150+
<AvatarFallback className="text-xs bg-neutral-200 text-neutral-700 font-medium">
151+
{getInitials(person.name)}
152+
</AvatarFallback>
153+
</Avatar>
154+
</div>
155+
<button
156+
onClick={(e) => {
157+
e.stopPropagation();
158+
handleRemoveSelected(person.id);
159+
}}
160+
className="flex items-center justify-center text-red-400 hover:text-red-600 absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity bg-white shadow-sm"
161+
>
162+
<CircleMinus className="size-4" />
163+
</button>
164+
</div>
165+
<div className="flex flex-col min-w-0">
166+
<span className="text-sm font-medium text-neutral-700 truncate">
167+
{person.name}
168+
</span>
169+
{person.email && (
170+
<span className="text-xs text-neutral-500 truncate">{person.email}</span>
171+
)}
172+
</div>
173+
</div>
174+
</div>
175+
))}
176+
</div>
177+
</div>
178+
)}
179+
180+
{selectedPeople.length > 0 && (
181+
<Button onClick={handleInvite} className="w-full">
182+
Invite {selectedPeople.length} {selectedPeople.length === 1 ? "person" : "people"}
183+
</Button>
184+
)}
185+
186+
{selectedPeople.length > 0 && <Separator />}
187+
188+
{invitedPeople.length > 0 && (
189+
<div className="flex flex-col gap-2">
190+
<div className="text-xs font-medium text-neutral-500">
191+
People with access
192+
</div>
193+
<div className="flex flex-col rounded-md border border-neutral-100 bg-neutral-50 overflow-hidden max-h-[40vh] overflow-y-auto">
194+
{invitedPeople.map((person) => (
195+
<div
196+
key={person.id}
197+
className="flex items-center justify-between gap-3 py-2 px-3 hover:bg-neutral-100 group transition-colors"
198+
>
199+
<div className="flex items-center gap-2.5 relative min-w-0 flex-1">
200+
<div className="relative size-7 flex items-center justify-center flex-shrink-0">
201+
<div className="absolute inset-0 flex items-center justify-center transition-opacity group-hover:opacity-0">
202+
<Avatar className="size-7">
203+
<AvatarFallback className="text-xs bg-neutral-200 text-neutral-700 font-medium">
204+
{getInitials(person.name)}
205+
</AvatarFallback>
206+
</Avatar>
207+
</div>
208+
<button
209+
onClick={(e) => {
210+
e.stopPropagation();
211+
handleRemovePerson(person.id);
212+
}}
213+
className="flex items-center justify-center text-red-400 hover:text-red-600 absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity bg-white shadow-sm"
214+
>
215+
<CircleMinus className="size-4" />
216+
</button>
217+
</div>
218+
<div className="flex flex-col min-w-0">
219+
<span className="text-sm font-medium text-neutral-700 truncate">
220+
{person.name}
221+
</span>
222+
{person.email && (
223+
<span className="text-xs text-neutral-500 truncate">{person.email}</span>
224+
)}
225+
</div>
226+
</div>
227+
228+
<Select
229+
value={person.role}
230+
onValueChange={(value: "viewer" | "editor") =>
231+
handleRoleChange(person.id, value)}
232+
>
233+
<SelectTrigger
234+
className="w-[100px] h-8 text-xs focus:ring-0 focus:ring-offset-0"
235+
onClick={(e) => e.stopPropagation()}
236+
>
237+
<SelectValue />
238+
</SelectTrigger>
239+
<SelectContent>
240+
<SelectItem value="viewer">Viewer</SelectItem>
241+
<SelectItem value="editor">Editor</SelectItem>
242+
</SelectContent>
243+
</Select>
244+
</div>
245+
))}
246+
</div>
247+
</div>
248+
)}
249+
250+
<Button
251+
variant="outline"
252+
className="w-full"
253+
onClick={handleCopyLink}
254+
>
255+
<Link2Icon className="size-4" />
256+
Copy link
257+
</Button>
258+
</div>
259+
</PopoverContent>
260+
</Popover>
8261
);
9262
}

0 commit comments

Comments
 (0)