Skip to content

Commit 1b097ea

Browse files
feat: added a function to give unique icons based on content type of resource
1 parent 04a26b9 commit 1b097ea

2 files changed

Lines changed: 151 additions & 3 deletions

File tree

app/lib/helpers/fileIcons.tsx

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,130 @@ import {
66
FolderIcon,
77
PhotoIcon,
88
DocumentIcon,
9+
DocumentTextIcon,
10+
CodeBracketIcon,
11+
VideoCameraIcon,
12+
MusicalNoteIcon,
13+
PresentationChartBarIcon,
14+
TableCellsIcon,
15+
ArchiveBoxIcon,
916
} from "@heroicons/react/24/outline";
1017

1118
export type FileType = "folder" | "file" | "image" | "document" | "other";
1219

20+
/**
21+
* Determines the icon based on MIME type
22+
* @param mimeType - The MIME type of the file
23+
* @returns React component for the file icon with appropriate color
24+
*/
25+
function getIconFromMimeType(mimeType: string) {
26+
const normalizedMime = mimeType.toLowerCase().split(";")[0].trim();
27+
28+
// Images
29+
if (normalizedMime.startsWith("image/")) {
30+
return <PhotoIcon className="h-6 w-6 text-green-500" />;
31+
}
32+
33+
// PDFs
34+
if (normalizedMime === "application/pdf") {
35+
return <DocumentTextIcon className="h-6 w-6 text-blue-500" />;
36+
}
37+
38+
// Videos
39+
if (normalizedMime.startsWith("video/")) {
40+
return <VideoCameraIcon className="h-6 w-6 text-purple-500" />;
41+
}
42+
43+
// Audio
44+
if (normalizedMime.startsWith("audio/")) {
45+
return <MusicalNoteIcon className="h-6 w-6 text-pink-500" />;
46+
}
47+
48+
// Code files
49+
if (
50+
normalizedMime.startsWith("text/") ||
51+
normalizedMime === "application/json" ||
52+
normalizedMime === "application/xml" ||
53+
normalizedMime === "text/xml" ||
54+
normalizedMime === "application/javascript" ||
55+
normalizedMime === "application/x-javascript" ||
56+
normalizedMime === "text/javascript" ||
57+
normalizedMime === "application/x-sh" ||
58+
normalizedMime === "application/x-yaml" ||
59+
normalizedMime === "text/yaml" ||
60+
normalizedMime === "text/css" ||
61+
normalizedMime === "text/html" ||
62+
normalizedMime === "text/markdown" ||
63+
normalizedMime === "text/plain"
64+
) {
65+
return <CodeBracketIcon className="h-6 w-6 text-orange-500" />;
66+
}
67+
68+
// Office documents - Word
69+
if (
70+
normalizedMime === "application/msword" ||
71+
normalizedMime === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
72+
normalizedMime === "application/vnd.ms-word.document.macroEnabled.12"
73+
) {
74+
return <DocumentIcon className="h-6 w-6 text-blue-600" />;
75+
}
76+
77+
// Office documents - Excel
78+
if (
79+
normalizedMime === "application/vnd.ms-excel" ||
80+
normalizedMime === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
81+
) {
82+
return <TableCellsIcon className="h-6 w-6 text-green-600" />;
83+
}
84+
85+
// Office documents - PowerPoint
86+
if (
87+
normalizedMime === "application/vnd.ms-powerpoint" ||
88+
normalizedMime === "application/vnd.openxmlformats-officedocument.presentationml.presentation"
89+
) {
90+
return <PresentationChartBarIcon className="h-6 w-6 text-orange-600" />;
91+
}
92+
93+
// Archives
94+
if (
95+
normalizedMime === "application/zip" ||
96+
normalizedMime === "application/x-zip-compressed" ||
97+
normalizedMime === "application/x-rar-compressed" ||
98+
normalizedMime === "application/x-tar" ||
99+
normalizedMime === "application/gzip" ||
100+
normalizedMime === "application/x-7z-compressed"
101+
) {
102+
return <ArchiveBoxIcon className="h-6 w-6 text-amber-600" />;
103+
}
104+
105+
// CSV files
106+
if (normalizedMime === "text/csv" || normalizedMime === "application/x-csv") {
107+
return <TableCellsIcon className="h-6 w-6 text-green-500" />;
108+
}
109+
110+
// Default document icon for other types
111+
return <DocumentIcon className="h-6 w-6 text-blue-500" />;
112+
}
113+
13114
/**
14115
* Returns the appropriate icon component for a given file type
15116
* @param type - The type of file (folder, image, document, etc.)
16-
* @param mimeType - Optional MIME type for more specific icon selection
117+
* @param mimeType - Optional MIME type for more specific icon selection (takes priority)
17118
* @returns React component for the file icon
18119
*/
19120
export function getFileIcon(type: FileType, mimeType?: string) {
121+
// Always show folder icon for folders
122+
if (type === "folder") {
123+
return <FolderIcon className="h-6 w-6 text-yellow-500" />;
124+
}
125+
126+
// If MIME type is provided, use it for more specific icon selection
127+
if (mimeType) {
128+
return getIconFromMimeType(mimeType);
129+
}
130+
131+
// Fallback to type-based icons if no MIME type
20132
switch (type) {
21-
case "folder":
22-
return <FolderIcon className="h-6 w-6 text-yellow-500" />;
23133
case "image":
24134
return <PhotoIcon className="h-6 w-6 text-green-500" />;
25135
case "document":

app/lib/hooks/useBrowseStorage.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function useBrowseStorage(containerUrl: string | null, refreshKey?: numbe
8181
let name = extractNameFromUrl(absoluteUrl);
8282
let lastModified: Date | undefined;
8383
let size: number | undefined;
84+
let mimeType: string | undefined;
8485

8586
// Check RDF metadata from container dataset- using getThing because it reads a resource (thing) from the RDF dataset to access properties like dcterms:title, rdfs:label, dcterms:modified, posix:size
8687
const itemThing = getThing(containerDataset, absoluteUrl);
@@ -132,6 +133,20 @@ export function useBrowseStorage(containerUrl: string | null, refreshKey?: numbe
132133
// otherwise only treat as container if URL explicitly ends with "/"
133134
finalIsContainer = isContainerUrl;
134135
}
136+
137+
// Extract MIME type from IANA media type URIs in RDF types
138+
// Pattern: http://www.w3.org/ns/iana/media-types/{mime-type}#Resource
139+
// Example: http://www.w3.org/ns/iana/media-types/image/png#Resource -> image/png
140+
if (!finalIsContainer && !mimeType) {
141+
const ianaMediaTypePattern = /^http:\/\/www\.w3\.org\/ns\/iana\/media-types\/(.+)#Resource$/;
142+
for (const type of types) {
143+
const match = type.match(ianaMediaTypePattern);
144+
if (match && match[1]) {
145+
mimeType = match[1];
146+
break; // Use the first IANA media type found
147+
}
148+
}
149+
}
135150
} else {
136151
// If no RDF metadata available
137152
if (isContainerUrl) {
@@ -143,13 +158,36 @@ export function useBrowseStorage(containerUrl: string | null, refreshKey?: numbe
143158
}
144159
}
145160

161+
// For non-folder files, only fetch content-type if not already found in RDF metadata
162+
if (!finalIsContainer && !mimeType) {
163+
try {
164+
const headResponse = await cacheBustingFetch(absoluteUrl, {
165+
method: "HEAD",
166+
headers: {
167+
Accept: "*/*",
168+
},
169+
});
170+
171+
if (headResponse.ok) {
172+
const contentType = headResponse.headers.get("Content-Type");
173+
if (contentType) {
174+
// Extract just the MIME type (remove charset, etc.)
175+
mimeType = contentType.split(";")[0].trim();
176+
}
177+
}
178+
} catch (err) {
179+
console.debug(`Could not fetch content-type for ${absoluteUrl}:`, err);
180+
}
181+
}
182+
146183
fileItems.push({
147184
id: absoluteUrl,
148185
name,
149186
type: finalIsContainer ? "folder" : "file",
150187
url: absoluteUrl,
151188
lastModified,
152189
size,
190+
mimeType,
153191
});
154192
} catch (err) {
155193
console.error(`Failed to process item ${itemUrl}:`, err);

0 commit comments

Comments
 (0)