Skip to content

Commit 2a3c9dd

Browse files
Merge pull request #2 from solid/feat/login
feat: build login flow, with oidc issuer integration, refactor codeba…
2 parents fa42552 + a012b85 commit 2a3c9dd

26 files changed

Lines changed: 1158 additions & 483 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ yarn-error.log*
3939
# typescript
4040
*.tsbuildinfo
4141
next-env.d.ts
42+
.history

app/components/AuthWrapper.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import {
5+
getDefaultSession,
6+
handleIncomingRedirect,
7+
} from "@inrupt/solid-client-authn-browser";
8+
import LoginPage from "./LoginPage";
9+
import LoadingSpinner from "./shared/LoadingSpinner";
10+
import ErrorDisplay from "./shared/ErrorDisplay";
11+
12+
interface AuthWrapperProps {
13+
children: React.ReactNode;
14+
}
15+
16+
export default function AuthWrapper({ children }: AuthWrapperProps) {
17+
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
18+
const [isChecking, setIsChecking] = useState(true);
19+
const [error, setError] = useState<Error | null>(null);
20+
21+
useEffect(() => {
22+
async function checkAuth() {
23+
try {
24+
setError(null);
25+
await handleIncomingRedirect();
26+
const session = getDefaultSession();
27+
setIsAuthenticated(session.info.isLoggedIn);
28+
} catch (err) {
29+
console.error("Auth check failed:", err);
30+
const errorMessage =
31+
err instanceof Error ? err : new Error("Authentication check failed");
32+
setError(errorMessage);
33+
setIsAuthenticated(false);
34+
} finally {
35+
setIsChecking(false);
36+
}
37+
}
38+
39+
checkAuth();
40+
}, []);
41+
42+
// Re-check authentication state periodically in case user logs in from another tab
43+
useEffect(() => {
44+
if (!isAuthenticated && !error) {
45+
const interval = setInterval(async () => {
46+
try {
47+
await handleIncomingRedirect();
48+
const session = getDefaultSession();
49+
if (session.info.isLoggedIn) {
50+
setIsAuthenticated(true);
51+
setError(null);
52+
}
53+
} catch (err) {
54+
console.error("Auth polling failed:", err);
55+
}
56+
}, 1000);
57+
58+
return () => clearInterval(interval);
59+
}
60+
}, [isAuthenticated, error]);
61+
62+
const handleRetry = () => {
63+
setError(null);
64+
setIsChecking(true);
65+
setIsAuthenticated(null);
66+
// Trigger re-check
67+
handleIncomingRedirect()
68+
.then(() => {
69+
const session = getDefaultSession();
70+
setIsAuthenticated(session.info.isLoggedIn);
71+
})
72+
.catch((err) => {
73+
const errorMessage =
74+
err instanceof Error ? err : new Error("Authentication check failed");
75+
setError(errorMessage);
76+
setIsAuthenticated(false);
77+
})
78+
.finally(() => {
79+
setIsChecking(false);
80+
});
81+
};
82+
83+
if (isChecking) {
84+
return (
85+
<div className="flex min-h-screen items-center justify-center bg-white">
86+
<LoadingSpinner size="md" text="Loading..." />
87+
</div>
88+
);
89+
}
90+
91+
if (error) {
92+
return (
93+
<ErrorDisplay
94+
title="Authentication Error"
95+
message={error.message || "Failed to authenticate. Please try again."}
96+
onRetry={handleRetry}
97+
/>
98+
);
99+
}
100+
101+
if (!isAuthenticated) {
102+
return <LoginPage />;
103+
}
104+
105+
return <>{children}</>;
106+
}
107+

app/components/Breadcrumb.tsx

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { ChevronRightIcon } from "@heroicons/react/24/outline";
4+
35
interface BreadcrumbItem {
46
name: string;
57
path: string;
@@ -27,20 +29,7 @@ export default function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
2729
>
2830
{items[0].name}
2931
</button>
30-
<svg
31-
className="h-4 w-4 flex-shrink-0 text-gray-400"
32-
fill="none"
33-
stroke="currentColor"
34-
viewBox="0 0 24 24"
35-
aria-hidden="true"
36-
>
37-
<path
38-
strokeLinecap="round"
39-
strokeLinejoin="round"
40-
strokeWidth={2}
41-
d="M9 5l7 7-7 7"
42-
/>
43-
</svg>
32+
<ChevronRightIcon className="h-4 w-4 flex-shrink-0 text-gray-400" />
4433
</li>
4534
<li className="text-sm text-gray-400">...</li>
4635
</>
@@ -50,29 +39,15 @@ export default function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
5039
return (
5140
<li key={item.path} className="flex items-center gap-1 sm:gap-2">
5241
{actualIndex > 0 && (
53-
<svg
54-
className="h-4 w-4 flex-shrink-0 text-gray-400"
55-
fill="none"
56-
stroke="currentColor"
57-
viewBox="0 0 24 24"
58-
aria-hidden="true"
59-
>
60-
<path
61-
strokeLinecap="round"
62-
strokeLinejoin="round"
63-
strokeWidth={2}
64-
d="M9 5l7 7-7 7"
65-
/>
66-
</svg>
42+
<ChevronRightIcon className="h-4 w-4 flex-shrink-0 text-gray-400" />
6743
)}
6844
<button
6945
type="button"
7046
onClick={() => onNavigate(item.path)}
71-
className={`cursor-pointer truncate text-sm ${
72-
actualIndex === items.length - 1
47+
className={`cursor-pointer truncate text-sm ${actualIndex === items.length - 1
7348
? "font-medium text-black"
7449
: "text-gray-600 hover:text-black"
75-
}`}
50+
}`}
7651
aria-current={actualIndex === items.length - 1 ? "page" : undefined}
7752
>
7853
{item.name}

app/components/FileItem.tsx

Lines changed: 17 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"use client";
22

33
import { useState } from "react";
4+
import Button from "./shared/Button";
5+
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
6+
import { getFileIcon, formatFileSize, formatDate, type FileType } from "../lib/helpers";
47

5-
export type FileType = "folder" | "file" | "image" | "document" | "other";
8+
export type { FileType };
69

710
export interface FileItemData {
811
id: string;
@@ -22,95 +25,6 @@ interface FileItemProps {
2225
isSelected?: boolean;
2326
}
2427

25-
function getFileIcon(type: FileType, mimeType?: string) {
26-
switch (type) {
27-
case "folder":
28-
return (
29-
<svg
30-
className="h-6 w-6 text-yellow-500"
31-
fill="currentColor"
32-
viewBox="0 0 24 24"
33-
aria-hidden="true"
34-
>
35-
<path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2h-8l-2-2z" />
36-
</svg>
37-
);
38-
case "image":
39-
return (
40-
<svg
41-
className="h-6 w-6 text-green-500"
42-
fill="none"
43-
stroke="currentColor"
44-
viewBox="0 0 24 24"
45-
aria-hidden="true"
46-
>
47-
<path
48-
strokeLinecap="round"
49-
strokeLinejoin="round"
50-
strokeWidth={2}
51-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
52-
/>
53-
</svg>
54-
);
55-
case "document":
56-
return (
57-
<svg
58-
className="h-6 w-6 text-blue-500"
59-
fill="none"
60-
stroke="currentColor"
61-
viewBox="0 0 24 24"
62-
aria-hidden="true"
63-
>
64-
<path
65-
strokeLinecap="round"
66-
strokeLinejoin="round"
67-
strokeWidth={2}
68-
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
69-
/>
70-
</svg>
71-
);
72-
default:
73-
return (
74-
<svg
75-
className="h-6 w-6 text-gray-500"
76-
fill="none"
77-
stroke="currentColor"
78-
viewBox="0 0 24 24"
79-
aria-hidden="true"
80-
>
81-
<path
82-
strokeLinecap="round"
83-
strokeLinejoin="round"
84-
strokeWidth={2}
85-
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
86-
/>
87-
</svg>
88-
);
89-
}
90-
}
91-
92-
function formatFileSize(bytes?: number): string {
93-
if (!bytes) return "";
94-
if (bytes < 1024) return `${bytes} B`;
95-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
96-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
97-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
98-
}
99-
100-
function formatDate(date?: Date): string {
101-
if (!date) return "";
102-
const now = new Date();
103-
const diff = now.getTime() - date.getTime();
104-
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
105-
106-
if (days === 0) return "Today";
107-
if (days === 1) return "Yesterday";
108-
if (days < 7) return `${days} days ago`;
109-
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
110-
111-
return date.toLocaleDateString();
112-
}
113-
11428
export default function FileItem({
11529
file,
11630
view,
@@ -122,12 +36,11 @@ export default function FileItem({
12236

12337
if (view === "grid") {
12438
return (
125-
<div
126-
className={`group relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 p-2 transition-colors sm:p-4 ${
127-
isSelected
128-
? "border-purple-500 bg-purple-50"
39+
<section
40+
className={`group relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 p-2 transition-colors sm:p-4 ${isSelected
41+
? "border-[#7B42F6] bg-[#F9F6FF]"
12942
: "border-transparent bg-white hover:border-gray-300 hover:bg-gray-50"
130-
}`}
43+
}`}
13144
onMouseEnter={() => setIsHovered(true)}
13245
onMouseLeave={() => setIsHovered(false)}
13346
onClick={() => onSelect(file)}
@@ -142,16 +55,15 @@ export default function FileItem({
14255
<p className="max-w-full truncate text-center text-xs font-medium text-black sm:text-sm">
14356
{file.name}
14457
</p>
145-
</div>
58+
</section>
14659
);
14760
}
14861

14962
// List view
15063
return (
151-
<div
152-
className={`group flex cursor-pointer items-center gap-2 border-b border-gray-100 px-2 py-2 transition-colors sm:gap-4 sm:px-4 sm:py-3 ${
153-
isSelected ? "bg-purple-50" : "bg-white hover:bg-gray-50"
154-
}`}
64+
<section
65+
className={`group flex cursor-pointer items-center gap-2 border-b border-gray-100 px-2 py-2 transition-colors sm:gap-4 sm:px-4 sm:py-3 ${isSelected ? "bg-[#F9F6FF]" : "bg-white hover:bg-gray-50"
66+
}`}
15567
onMouseEnter={() => setIsHovered(true)}
15668
onMouseLeave={() => setIsHovered(false)}
15769
onClick={() => onSelect(file)}
@@ -174,33 +86,19 @@ export default function FileItem({
17486
</div>
17587
{isHovered && (
17688
<div className="flex-shrink-0">
177-
<button
178-
type="button"
179-
className="cursor-pointer rounded-md p-1 text-gray-600 hover:bg-gray-200"
89+
<Button
90+
variant="icon"
18091
aria-label="More options"
18192
onClick={(e) => {
18293
e.stopPropagation();
18394
// Handle more options
18495
}}
18596
>
186-
<svg
187-
className="h-4 w-4 sm:h-5 sm:w-5"
188-
fill="none"
189-
stroke="currentColor"
190-
viewBox="0 0 24 24"
191-
aria-hidden="true"
192-
>
193-
<path
194-
strokeLinecap="round"
195-
strokeLinejoin="round"
196-
strokeWidth={2}
197-
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
198-
/>
199-
</svg>
200-
</button>
97+
<EllipsisVerticalIcon className="h-4 w-4 sm:h-5 sm:w-5" />
98+
</Button>
20199
</div>
202100
)}
203-
</div>
101+
</section>
204102
);
205103
}
206104

0 commit comments

Comments
 (0)