Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.history
107 changes: 107 additions & 0 deletions app/components/AuthWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { useEffect, useState } from "react";
import {
getDefaultSession,
handleIncomingRedirect,
} from "@inrupt/solid-client-authn-browser";
import LoginPage from "./LoginPage";
import LoadingSpinner from "./shared/LoadingSpinner";
import ErrorDisplay from "./shared/ErrorDisplay";

interface AuthWrapperProps {
children: React.ReactNode;
}

export default function AuthWrapper({ children }: AuthWrapperProps) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [isChecking, setIsChecking] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
async function checkAuth() {
try {
setError(null);
await handleIncomingRedirect();
const session = getDefaultSession();
setIsAuthenticated(session.info.isLoggedIn);
} catch (err) {
console.error("Auth check failed:", err);
const errorMessage =
err instanceof Error ? err : new Error("Authentication check failed");
setError(errorMessage);
setIsAuthenticated(false);
} finally {
setIsChecking(false);
}
}

checkAuth();
}, []);

// Re-check authentication state periodically in case user logs in from another tab
useEffect(() => {
if (!isAuthenticated && !error) {
const interval = setInterval(async () => {
try {
await handleIncomingRedirect();
const session = getDefaultSession();
if (session.info.isLoggedIn) {
setIsAuthenticated(true);
setError(null);
}
} catch (err) {
console.error("Auth polling failed:", err);
}
}, 1000);

return () => clearInterval(interval);
}
}, [isAuthenticated, error]);

const handleRetry = () => {
setError(null);
setIsChecking(true);
setIsAuthenticated(null);
// Trigger re-check
handleIncomingRedirect()
.then(() => {
const session = getDefaultSession();
setIsAuthenticated(session.info.isLoggedIn);
})
.catch((err) => {
const errorMessage =
err instanceof Error ? err : new Error("Authentication check failed");
setError(errorMessage);
setIsAuthenticated(false);
})
.finally(() => {
setIsChecking(false);
});
};

if (isChecking) {
return (
<div className="flex min-h-screen items-center justify-center bg-white">
<LoadingSpinner size="md" text="Loading..." />
</div>
);
}

if (error) {
return (
<ErrorDisplay
title="Authentication Error"
message={error.message || "Failed to authenticate. Please try again."}
onRetry={handleRetry}
/>
);
}

if (!isAuthenticated) {
return <LoginPage />;
}

return <>{children}</>;
}

37 changes: 6 additions & 31 deletions app/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { ChevronRightIcon } from "@heroicons/react/24/outline";

interface BreadcrumbItem {
name: string;
path: string;
Expand Down Expand Up @@ -27,20 +29,7 @@ export default function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
>
{items[0].name}
</button>
<svg
className="h-4 w-4 flex-shrink-0 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
<ChevronRightIcon className="h-4 w-4 flex-shrink-0 text-gray-400" />
</li>
<li className="text-sm text-gray-400">...</li>
</>
Expand All @@ -50,29 +39,15 @@ export default function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
return (
<li key={item.path} className="flex items-center gap-1 sm:gap-2">
{actualIndex > 0 && (
<svg
className="h-4 w-4 flex-shrink-0 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
<ChevronRightIcon className="h-4 w-4 flex-shrink-0 text-gray-400" />
)}
<button
type="button"
onClick={() => onNavigate(item.path)}
className={`cursor-pointer truncate text-sm ${
actualIndex === items.length - 1
className={`cursor-pointer truncate text-sm ${actualIndex === items.length - 1
? "font-medium text-black"
: "text-gray-600 hover:text-black"
}`}
}`}
aria-current={actualIndex === items.length - 1 ? "page" : undefined}
>
{item.name}
Expand Down
136 changes: 17 additions & 119 deletions app/components/FileItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"use client";

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

export type FileType = "folder" | "file" | "image" | "document" | "other";
export type { FileType };

export interface FileItemData {
id: string;
Expand All @@ -22,95 +25,6 @@ interface FileItemProps {
isSelected?: boolean;
}

function getFileIcon(type: FileType, mimeType?: string) {
switch (type) {
case "folder":
return (
<svg
className="h-6 w-6 text-yellow-500"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<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" />
</svg>
);
case "image":
return (
<svg
className="h-6 w-6 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
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"
/>
</svg>
);
case "document":
return (
<svg
className="h-6 w-6 text-blue-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
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"
/>
</svg>
);
default:
return (
<svg
className="h-6 w-6 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
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"
/>
</svg>
);
}
}

function formatFileSize(bytes?: number): string {
if (!bytes) return "";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}

function formatDate(date?: Date): string {
if (!date) return "";
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));

if (days === 0) return "Today";
if (days === 1) return "Yesterday";
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;

return date.toLocaleDateString();
}

export default function FileItem({
file,
view,
Expand All @@ -122,12 +36,11 @@ export default function FileItem({

if (view === "grid") {
return (
<div
className={`group relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 p-2 transition-colors sm:p-4 ${
isSelected
? "border-purple-500 bg-purple-50"
<section
className={`group relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 p-2 transition-colors sm:p-4 ${isSelected
? "border-[#7B42F6] bg-[#F9F6FF]"
: "border-transparent bg-white hover:border-gray-300 hover:bg-gray-50"
}`}
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => onSelect(file)}
Expand All @@ -142,16 +55,15 @@ export default function FileItem({
<p className="max-w-full truncate text-center text-xs font-medium text-black sm:text-sm">
{file.name}
</p>
</div>
</section>
);
}

// List view
return (
<div
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-purple-50" : "bg-white hover:bg-gray-50"
}`}
<section
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"
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => onSelect(file)}
Expand All @@ -174,33 +86,19 @@ export default function FileItem({
</div>
{isHovered && (
<div className="flex-shrink-0">
<button
type="button"
className="cursor-pointer rounded-md p-1 text-gray-600 hover:bg-gray-200"
<Button
variant="icon"
aria-label="More options"
onClick={(e) => {
e.stopPropagation();
// Handle more options
}}
>
<svg
className="h-4 w-4 sm:h-5 sm:w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
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"
/>
</svg>
</button>
<EllipsisVerticalIcon className="h-4 w-4 sm:h-5 sm:w-5" />
</Button>
</div>
)}
</div>
</section>
);
}

Loading