Skip to content

Commit 7aa21c3

Browse files
committed
UX improvements / Styling fixes
* Added item sorting * Fully implemented item searching * Made it so creating a new password will auto select the new one
1 parent 831260e commit 7aa21c3

File tree

3 files changed

+158
-15
lines changed

3 files changed

+158
-15
lines changed

src/app/(main)/page.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import {VaultPage} from "@/components/vault/vault-page";
2-
import {auth, currentUser} from "@clerk/nextjs/server";
3-
import {getPasswords, instantiateVault} from "../actions";
1+
import { VaultPage } from "@/components/vault/vault-page";
2+
import { auth, currentUser } from "@clerk/nextjs/server";
3+
import { getPasswords, instantiateVault } from "../actions";
44

5-
export default async function Page(){
5+
export default async function Page() {
66
const { userId, redirectToSignIn } = await auth();
77

88
if (!userId) return redirectToSignIn();
99

10-
const user = await getPasswords(userId)
10+
const user = await getPasswords(userId);
1111

1212
if (!user) {
13-
const clerkUser = await currentUser()
14-
if(!clerkUser) return redirectToSignIn()
15-
await instantiateVault(clerkUser.id, clerkUser.username as string);
13+
const clerkUser = await currentUser();
14+
if (!clerkUser) return redirectToSignIn();
15+
await instantiateVault(clerkUser.id, clerkUser.username as string);
1616
}
1717

1818
return <VaultPage user={user} />;
19-
};
19+
}

src/components/vault/dialogs/create-password-dialog.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,28 @@ const initialPasswordItemState = {
2424
password: "",
2525
};
2626

27+
interface PasswordEntry {
28+
id: string;
29+
name: string;
30+
username: string;
31+
website: string;
32+
password: string;
33+
usernameIV: string;
34+
websiteIV: string;
35+
passwordIV: string;
36+
updatedAt: string;
37+
lastAccess: string;
38+
created: string;
39+
}
40+
2741
export const CreatePasswordDialog = ({
2842
open,
2943
onClose,
44+
setSelectedEntry,
3045
}: {
3146
open: boolean;
3247
onClose: () => void;
48+
setSelectedEntry: (entry: PasswordEntry) => void;
3349
}) => {
3450
const [passwordItem, setPasswordItem] = useState(initialPasswordItemState);
3551
const [loading, setLoading] = useState(false);
@@ -88,7 +104,7 @@ export const CreatePasswordDialog = ({
88104
clerkuser.id
89105
);
90106

91-
await createPasswordItem(
107+
const item = await createPasswordItem(
92108
encryptedUsername.encryptedData,
93109
encryptedWebsite.encryptedData,
94110
encryptedPassword.encryptedData,
@@ -97,8 +113,23 @@ export const CreatePasswordDialog = ({
97113
encryptedPassword.iv
98114
);
99115

116+
const passwordEntry: PasswordEntry = {
117+
id: item.id,
118+
name: passwordItem.name,
119+
username: passwordItem.username,
120+
website: passwordItem.website,
121+
password: passwordItem.password,
122+
usernameIV: item.usernameIV,
123+
websiteIV: item.websiteIV,
124+
passwordIV: item.passwordIV,
125+
updatedAt: item.updatedAt.toISOString(),
126+
lastAccess: item.updatedAt.toISOString(),
127+
created: item.createdAt.toISOString()
128+
};
129+
100130
toast.success("Password created");
101131
setPasswordItem(initialPasswordItemState);
132+
setSelectedEntry(passwordEntry);
102133
onClose();
103134
} catch (error) {
104135
toast.error("Failed to create password");

src/components/vault/vault-page.tsx

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@ import { cn } from "@/lib/utils";
1212
import { decrypt, generateAndStoreKey, retrieveKey } from "@/utils/encryption";
1313
import { useUser } from "@clerk/nextjs";
1414
import type { Prisma } from "@prisma/client";
15-
import { Plus, SquareArrowOutUpRight, Trash, User } from "lucide-react";
15+
import {
16+
Plus,
17+
SquareArrowOutUpRight,
18+
Trash,
19+
User,
20+
ArrowDownAZ,
21+
ArrowDownWideNarrow,
22+
Clock,
23+
} from "lucide-react";
1624
import Image from "next/image";
1725
import { useRouter } from "next/navigation";
1826
import { useEffect, useState } from "react";
@@ -27,6 +35,7 @@ import {
2735
import { EmptyState } from "./empty-state";
2836
import { PasswordDetails } from "./password-details";
2937
import { Sidebar } from "./sidebar";
38+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
3039

3140
interface PasswordEntry {
3241
id: string;
@@ -65,9 +74,11 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
6574
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
6675
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
6776
const [searchQuery, setSearchQuery] = useState("");
68-
const [filteredEntries, setFilteredEntries] = useState<PasswordEntry[]>([]);
6977
const [passwords, setPasswords] = useState<PasswordEntry[]>([]);
7078
const [passwordItems, setPasswordItems] = useState(user?.passwordItems);
79+
const [sortBy, setSortBy] = useState<"name" | "created" | "updated">(
80+
"created"
81+
);
7182

7283
useEffect(() => {
7384
const ensureEncryptionKey = async () => {
@@ -139,16 +150,59 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
139150
decryptPasswords();
140151
}, [user?.passwordItems, clerkUser, passwordItems]);
141152

153+
useEffect(() => {
154+
const sortPasswords = () => {
155+
const sorted = [...passwords].sort((a, b) => {
156+
switch (sortBy) {
157+
case "name":
158+
return a.name.localeCompare(b.name);
159+
case "updated":
160+
return (
161+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
162+
);
163+
default: // 'created'
164+
return (
165+
new Date(b.created).getTime() - new Date(a.created).getTime()
166+
);
167+
}
168+
});
169+
setPasswords(sorted);
170+
};
171+
sortPasswords();
172+
}, [sortBy]);
173+
142174
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
143175
setSearchQuery(e.target.value);
144-
// Filter entries based on search query
145-
// ... (filter entries logic)
146176
};
147177

148178
const handleEditEntry = () => {
149179
setIsEditDialogOpen(true);
150180
};
151181

182+
const filteredAndSortedPasswords = passwords
183+
.filter((password) => {
184+
if (!searchQuery) return true;
185+
186+
const search = searchQuery.toLowerCase();
187+
return (
188+
password.name.toLowerCase().includes(search) ||
189+
password.username.toLowerCase().includes(search) ||
190+
password.website.toLowerCase().includes(search)
191+
);
192+
})
193+
.sort((a, b) => {
194+
switch (sortBy) {
195+
case "name":
196+
return a.name.localeCompare(b.name);
197+
case "updated":
198+
return (
199+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
200+
);
201+
default: // 'created'
202+
return new Date(b.created).getTime() - new Date(a.created).getTime();
203+
}
204+
});
205+
152206
return (
153207
<div className="flex h-screen overflow-hidden bg-white dark:bg-gray-900">
154208
<div className="hidden lg:block lg:w-64 border-r border-gray-100 dark:border-gray-800">
@@ -182,6 +236,63 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
182236
onChange={handleSearchChange}
183237
className="w-48 focus:ring-0 ring-0 focus-visible:ring-0 dark:bg-gray-800 dark:text-white"
184238
/>
239+
<div className="w-1/3 overflow-y-auto">
240+
<div className="flex items-center gap-2 p-4">
241+
<Tooltip>
242+
<TooltipTrigger asChild>
243+
<Button
244+
variant="ghost"
245+
size="sm"
246+
className={cn(
247+
"h-8 w-8 p-0",
248+
sortBy === "name" &&
249+
"bg-rose-50 dark:bg-rose-900 text-rose-900 dark:text-rose-50"
250+
)}
251+
onClick={() => setSortBy("name")}
252+
>
253+
<ArrowDownAZ className="h-4 w-4" />
254+
</Button>
255+
</TooltipTrigger>
256+
<TooltipContent>Sort alphabetically</TooltipContent>
257+
</Tooltip>
258+
259+
<Tooltip>
260+
<TooltipTrigger asChild>
261+
<Button
262+
variant="ghost"
263+
size="sm"
264+
className={cn(
265+
"h-8 w-8 p-0",
266+
sortBy === "created" &&
267+
"bg-rose-50 dark:bg-rose-900 text-rose-900 dark:text-rose-50"
268+
)}
269+
onClick={() => setSortBy("created")}
270+
>
271+
<ArrowDownWideNarrow className="h-4 w-4" />
272+
</Button>
273+
</TooltipTrigger>
274+
<TooltipContent>Sort by created</TooltipContent>
275+
</Tooltip>
276+
277+
<Tooltip>
278+
<TooltipTrigger asChild>
279+
<Button
280+
variant="ghost"
281+
size="sm"
282+
className={cn(
283+
"h-8 w-8 p-0",
284+
sortBy === "updated" &&
285+
"bg-rose-50 dark:bg-rose-900 text-rose-900 dark:text-rose-50"
286+
)}
287+
onClick={() => setSortBy("updated")}
288+
>
289+
<Clock className="h-4 w-4" />
290+
</Button>
291+
</TooltipTrigger>
292+
<TooltipContent>Sort by updated</TooltipContent>
293+
</Tooltip>
294+
</div>
295+
</div>
185296
<Button
186297
size="icon"
187298
className="bg-rose-50 hover:hover:bg-rose-100 dark:bg-rose-900 dark:hover:bg-rose-800"
@@ -205,7 +316,7 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
205316
<ScrollArea className="h-full">
206317
<div className="space-y-2 p-4">
207318
{activeTab === "passwords" &&
208-
passwords.map((password) => (
319+
filteredAndSortedPasswords.map((password) => (
209320
<ContextMenu key={password.id}>
210321
<ContextMenuTrigger>
211322
<Button
@@ -363,6 +474,7 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
363474
const updatedItems = await getPasswords(user?.id as string);
364475
setPasswordItems(updatedItems?.passwordItems);
365476
}}
477+
setSelectedEntry={setSelectedEntry}
366478
/>
367479
</div>
368480
);

0 commit comments

Comments
 (0)