Skip to content

Commit 3fc5a5a

Browse files
Merge pull request #7 from solid/feat/my-storages
Feat/my storages
2 parents 47b1bfb + 46df242 commit 3fc5a5a

13 files changed

Lines changed: 382 additions & 263 deletions

app/components/AuthWrapper.tsx

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,48 +22,30 @@ export default function AuthWrapper({ children }: AuthWrapperProps) {
2222
async function checkAuth() {
2323
try {
2424
setError(null);
25-
26-
// Always handle incoming redirect first - this is necessary to restore sessions
25+
2726
// The library uses this to restore session state from localStorage
28-
// Set restorePreviousSession to true to enable session restoration on page refresh
2927
const redirectInfo = await handleIncomingRedirect({
3028
restorePreviousSession: true,
3129
});
32-
30+
3331
// Get the session instance after handling redirect
3432
const session = getDefaultSession();
35-
36-
// development logs (will remove later)
37-
console.log("=== Session Check ===");
38-
console.log("Redirect Info:", redirectInfo);
39-
console.log("Session Info:", session.info);
40-
console.log("Is Logged In:", session.info.isLoggedIn);
41-
console.log("WebID:", session.info.webId);
42-
console.log("Session ID:", session.info.sessionId);
43-
if (session.info.expirationDate) {
44-
const expDate = new Date(session.info.expirationDate);
45-
console.log("Expiration Date:", expDate.toISOString());
46-
console.log("Is Expired:", expDate <= new Date());
47-
}
48-
49-
// Check if we have a valid session
33+
5034
let isLoggedIn = session.info.isLoggedIn && !!session.info.webId;
51-
35+
5236
// Check expiration if session exists
5337
if (isLoggedIn && session.info.expirationDate) {
5438
const expirationDate = new Date(session.info.expirationDate);
5539
const now = new Date();
5640
if (expirationDate <= now) {
57-
console.log("Session expired, user needs to re-login");
5841
isLoggedIn = false;
5942
}
6043
}
61-
62-
63-
44+
45+
46+
6447
setIsAuthenticated(isLoggedIn);
6548
} catch (err) {
66-
console.error("Auth check failed:", err);
6749
const errorMessage =
6850
err instanceof Error ? err : new Error("Authentication check failed");
6951
setError(errorMessage);
@@ -78,28 +60,19 @@ export default function AuthWrapper({ children }: AuthWrapperProps) {
7860

7961
// Re-check authentication state periodically in case user logs in from another tab
8062
useEffect(() => {
81-
if (!isAuthenticated && !error) {
82-
const interval = setInterval(async () => {
83-
try {
84-
const redirectInfo = await handleIncomingRedirect({
85-
restorePreviousSession: true,
86-
});
87-
const session = getDefaultSession();
63+
if (!isAuthenticated && !error) {
64+
const interval = setInterval(async () => {
65+
try {
66+
const redirectInfo = await handleIncomingRedirect({
67+
restorePreviousSession: true,
68+
});
69+
const session = getDefaultSession();
8870
if (session.info.isLoggedIn) {
89-
// Log authentication response after redirect (from polling)
90-
console.log("=== Authentication Response (from polling) ===");
91-
console.log("Redirect Info:", redirectInfo);
92-
console.log("Session Info:", session.info);
93-
console.log("WebID:", session.info.webId);
94-
console.log("Is Logged In:", session.info.isLoggedIn);
95-
console.log("Session ID:", session.info.sessionId);
96-
console.log("==============================================");
97-
9871
setIsAuthenticated(true);
9972
setError(null);
10073
}
10174
} catch (err) {
102-
console.error("Auth polling failed:", err);
75+
// Silent fail for polling
10376
}
10477
}, 1000);
10578

@@ -111,7 +84,7 @@ export default function AuthWrapper({ children }: AuthWrapperProps) {
11184
setError(null);
11285
setIsChecking(true);
11386
setIsAuthenticated(null);
114-
// Trigger re-check
87+
11588
handleIncomingRedirect({ restorePreviousSession: true })
11689
.then(() => {
11790
const session = getDefaultSession();

app/components/Breadcrumb.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ export default function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
3636
)}
3737
{displayItems.slice(items.length > 2 ? 1 : 0).map((item, index) => {
3838
const actualIndex = items.length > 2 ? items.length - 2 + index : index;
39+
// Use a combination of path and index to ensure unique keys
3940
return (
40-
<li key={item.path} className="flex items-center gap-1 sm:gap-2">
41+
<li key={`${item.path}-${actualIndex}`} className="flex items-center gap-1 sm:gap-2">
4142
{actualIndex > 0 && (
4243
<ChevronRightIcon className="h-4 w-4 flex-shrink-0 text-gray-400" />
4344
)}

app/components/FileItem.tsx

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

3-
import { useState } from "react";
3+
import { useState, useRef } from "react";
44
import Button from "./shared/Button";
55
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
66
import { getFileIcon, formatFileSize, formatDate, type FileType } from "../lib/helpers";
@@ -33,6 +33,31 @@ export default function FileItem({
3333
isSelected = false,
3434
}: FileItemProps) {
3535
const [isHovered, setIsHovered] = useState(false);
36+
const clickCountRef = useRef(0);
37+
const clickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
38+
39+
const handleClick = (e: React.MouseEvent) => {
40+
clickCountRef.current += 1;
41+
42+
if (clickCountRef.current === 1) {
43+
clickTimeoutRef.current = setTimeout(() => {
44+
if (clickCountRef.current === 1) {
45+
onSelect(file);
46+
}
47+
clickCountRef.current = 0;
48+
clickTimeoutRef.current = null;
49+
}, 300);
50+
} else if (clickCountRef.current === 2) {
51+
if (clickTimeoutRef.current) {
52+
clearTimeout(clickTimeoutRef.current);
53+
clickTimeoutRef.current = null;
54+
}
55+
clickCountRef.current = 0;
56+
e.preventDefault();
57+
e.stopPropagation();
58+
onDoubleClick(file);
59+
}
60+
};
3661

3762
if (view === "grid") {
3863
return (
@@ -43,8 +68,7 @@ export default function FileItem({
4368
}`}
4469
onMouseEnter={() => setIsHovered(true)}
4570
onMouseLeave={() => setIsHovered(false)}
46-
onClick={() => onSelect(file)}
47-
onDoubleClick={() => onDoubleClick(file)}
71+
onClick={handleClick}
4872
role="button"
4973
tabIndex={0}
5074
aria-label={`${file.type === "folder" ? "Folder" : "File"}: ${file.name}`}
@@ -66,8 +90,7 @@ export default function FileItem({
6690
}`}
6791
onMouseEnter={() => setIsHovered(true)}
6892
onMouseLeave={() => setIsHovered(false)}
69-
onClick={() => onSelect(file)}
70-
onDoubleClick={() => onDoubleClick(file)}
93+
onClick={handleClick}
7194
role="button"
7295
tabIndex={0}
7396
aria-label={`${file.type === "folder" ? "Folder" : "File"}: ${file.name}`}
@@ -91,7 +114,6 @@ export default function FileItem({
91114
aria-label="More options"
92115
onClick={(e) => {
93116
e.stopPropagation();
94-
// Handle more options
95117
}}
96118
>
97119
<EllipsisVerticalIcon className="h-4 w-4 sm:h-5 sm:w-5" />

app/components/Header.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,14 @@ import ProfileIcon from "./ProfileIcon";
88
import {
99
Bars3Icon,
1010
MagnifyingGlassIcon,
11-
ShareIcon,
1211
PlusIcon,
1312
} from "@heroicons/react/24/outline";
1413

1514
interface HeaderProps {
16-
selectedFileCount?: number;
17-
onShareClick?: () => void;
1815
onMenuClick?: () => void;
1916
}
2017

21-
export default function Header({ selectedFileCount = 0, onShareClick, onMenuClick }: HeaderProps) {
18+
export default function Header({ onMenuClick }: HeaderProps) {
2219
const [searchQuery, setSearchQuery] = useState("");
2320

2421
return (
@@ -54,18 +51,6 @@ export default function Header({ selectedFileCount = 0, onShareClick, onMenuClic
5451

5552
{/* Action Buttons */}
5653
<div className="ml-auto flex items-center gap-1 sm:gap-2">
57-
{selectedFileCount > 0 && onShareClick && (
58-
<Button
59-
variant="primary"
60-
size="sm"
61-
onClick={onShareClick}
62-
className="flex h-9 items-center gap-1 sm:gap-2 sm:px-3"
63-
aria-label="Share selected files"
64-
>
65-
<ShareIcon className="h-4 w-4" />
66-
<span className="hidden sm:inline">Share</span>
67-
</Button>
68-
)}
6954
<Button
7055
variant="secondary"
7156
size="sm"

app/lib/helpers/breadcrumbUtils.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Breadcrumb utility functions for building navigation breadcrumbs
3+
*/
4+
5+
export interface BreadcrumbItem {
6+
name: string;
7+
path: string;
8+
}
9+
10+
/**
11+
* Builds breadcrumb items for navigation within a storage
12+
* @param selectedStorageId - The ID of the currently selected storage
13+
* @param selectedStorageUrl - The URL of the currently selected storage
14+
* @param selectedStorageName - The name of the currently selected storage
15+
* @param currentPath - The current path within the storage
16+
* @returns Array of breadcrumb items
17+
*/
18+
export function buildBreadcrumbItems(
19+
selectedStorageId: string | null,
20+
selectedStorageUrl: string | undefined,
21+
selectedStorageName: string | undefined,
22+
currentPath: string
23+
): BreadcrumbItem[] {
24+
if (!selectedStorageId) {
25+
return [{ name: "My Solid Storages", path: "/" }];
26+
}
27+
28+
const items: BreadcrumbItem[] = [
29+
{ name: "My Solid Storages", path: "/" },
30+
{ name: selectedStorageName || "Storage", path: selectedStorageUrl || "/" },
31+
];
32+
33+
if (currentPath !== "/" && currentPath !== selectedStorageUrl) {
34+
const storageUrl = selectedStorageUrl || "";
35+
const storagePath = new URL(storageUrl).pathname;
36+
const currentPathObj = new URL(currentPath);
37+
const relativePath = currentPathObj.pathname
38+
.replace(storagePath, "")
39+
.replace(/^\/|\/$/g, "");
40+
const segments = relativePath.split("/").filter(Boolean);
41+
42+
segments.forEach((segment, index) => {
43+
const pathSegments = segments.slice(0, index + 1);
44+
const path = storageUrl + pathSegments.join("/") + "/";
45+
items.push({ name: decodeURIComponent(segment), path });
46+
});
47+
}
48+
49+
return items;
50+
}
51+

app/lib/helpers/fileFilters.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
/**
66
* Checks if a URL or name represents a profile-related item that should be hidden from the file manager
7+
* Only filters out the actual profile card document, not the profile folder
78
* @param url - The URL to check
89
* @param name - Optional name to check (for additional filtering)
910
* @returns true if the item should be filtered out (is profile-related)
@@ -12,12 +13,13 @@ export function isProfileItem(url: string, name?: string): boolean {
1213
const urlLower = url.toLowerCase();
1314
const nameLower = name?.toLowerCase() || "";
1415

16+
// Only filter out the profile card document, not the profile folder
17+
// Profile card is typically at /profile/card or ends with /card
1518
return (
16-
urlLower.includes('/profile/') ||
17-
urlLower.includes('/card') ||
18-
urlLower.endsWith('/profile') ||
19-
urlLower.includes('profile/card') ||
20-
nameLower.includes('card')
19+
urlLower.includes('/profile/card') ||
20+
urlLower.endsWith('/card') ||
21+
(urlLower.includes('/card') && !urlLower.endsWith('/')) ||
22+
nameLower === 'card'
2123
);
2224
}
2325

app/lib/helpers/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
/**
2-
* Central export point for all helper functions
3-
* This allows importing multiple helpers from a single location
4-
*/
5-
61
export * from "./fileUtils";
72
export * from "./dateUtils";
83
export * from "./fileIcons";
94
export * from "./fileFilters";
5+
export * from "./urlUtils";
6+
export * from "./breadcrumbUtils";
107

app/lib/helpers/urlUtils.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* URL utility functions for processing and extracting information from URLs
3+
*/
4+
5+
/**
6+
* Extracts a display name from a URL
7+
* @param url - The URL to extract the name from
8+
* @returns The decoded name extracted from the URL path
9+
*/
10+
export function extractNameFromUrl(url: string): string {
11+
try {
12+
const urlObj = new URL(url);
13+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
14+
let name = pathParts[pathParts.length - 1] || urlObj.hostname;
15+
16+
try {
17+
name = decodeURIComponent(name);
18+
} catch (e) {
19+
// Keep original name if decoding fails
20+
}
21+
22+
return name;
23+
} catch (e) {
24+
// If URL parsing fails, try to extract from the string directly
25+
const parts = url.split("/").filter(Boolean);
26+
const lastPart = parts[parts.length - 1] || url;
27+
try {
28+
return decodeURIComponent(lastPart);
29+
} catch {
30+
return lastPart;
31+
}
32+
}
33+
}
34+
35+
/**
36+
* Resolves a relative URL to an absolute URL
37+
* @param url - The URL to resolve (may be relative or absolute)
38+
* @param baseUrl - The base URL to resolve against
39+
* @returns The absolute URL
40+
*/
41+
export function resolveUrl(url: string, baseUrl: string): string {
42+
try {
43+
return new URL(url, baseUrl).href;
44+
} catch (e) {
45+
return url;
46+
}
47+
}
48+
49+
/**
50+
* Checks if a URL has a file extension that indicates it's likely a file
51+
* @param url - The URL to check
52+
* @returns true if the URL has a known file extension
53+
*/
54+
export function isLikelyFile(url: string): boolean {
55+
const fileExtension = url.split('.').pop()?.toLowerCase();
56+
const knownFileExtensions = ['txt', 'md', 'json', 'xml', 'html', 'css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf'];
57+
return fileExtension ? knownFileExtensions.includes(fileExtension) : false;
58+
}
59+

app/lib/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export { useSolidStorages } from "./useSolidStorages";
1313
export type { SolidStorage } from "./useSolidStorages";
1414
export { useUserProfile } from "./useUserProfile";
1515
export type { UserProfile } from "./useUserProfile";
16+
export { useBrowseStorage } from "./useBrowseStorage";
1617

0 commit comments

Comments
 (0)