added more hooks

This commit is contained in:
subnub
2024-06-22 03:46:42 -04:00
parent ecca46c73e
commit 4cccd43677
11 changed files with 430 additions and 357 deletions
+2
View File
@@ -495,6 +495,8 @@ class FileController {
const userID = req.user._id;
const fileID = req.body.id;
console.log("id", fileID);
await this.chunkService.deleteFile(userID, fileID);
res.send();
@@ -172,7 +172,8 @@ class FileSystemService implements ChunkInterface {
const filePath = currentFile.metadata.filePath!;
const IV = currentFile.metadata.IV.buffer as Buffer;
const IV = currentFile.metadata.IV;
console.log("iv", IV);
const readStream = fs.createReadStream(filePath);
@@ -260,7 +261,7 @@ class FileSystemService implements ChunkInterface {
if (!file) throw new NotFoundError("File Thumbnail Not Found");
const password = user.getEncryptionKey();
const IV = file.metadata.IV.buffer as Buffer;
const IV = file.metadata.IV;
if (!password) throw new ForbiddenError("Invalid Encryption Key");
+35
View File
@@ -11,6 +11,8 @@ interface QueryKeyParams {
startAt?: boolean;
}
// GET
export const getFilesList = async ({
queryKey,
pageParam,
@@ -47,3 +49,36 @@ export const getQuickFilesList = async () => {
});
return response.data;
};
export const getFileThumbnail = async (thumbnailID: string) => {
const url = `http://localhost:5173/api/file-service/thumbnail/${thumbnailID}`;
const config = {
responseType: "arraybuffer",
};
const response = await axios.get(url, config);
const imgFile = new Blob([response.data]);
const imgUrl = URL.createObjectURL(imgFile);
return imgUrl;
};
// PATCH
export const renameFile = async (fileID: string, name: string) => {
const response = await axios.patch(`/file-service/rename`, {
id: fileID,
title: name,
});
return response.data;
};
// DELETE
export const deleteFile = async (fileID: string) => {
const response = await axios.delete(`/file-service/remove`, {
data: {
id: fileID,
},
});
return response.data;
};
+11 -2
View File
@@ -13,8 +13,11 @@ import { useFiles } from "../../hooks/files";
import { useFolders } from "../../hooks/folders";
const DataForm = (props) => {
const params = useParams();
const { data: files, fetchNextPage: filesFetchNextPage } = useFiles();
const {
data: files,
fetchNextPage: filesFetchNextPage,
invalidateFilesCache,
} = useFiles();
const { data: folders } = useFolders();
return (
@@ -39,6 +42,12 @@ const DataForm = (props) => {
>
Next page
</button>
<button
className="p-2 bg-blue-500 rounded-md text-white ml-4"
onClick={invalidateFilesCache}
>
Refresh
</button>
<div
className={
props.parent === "/"
+114 -81
View File
@@ -1,91 +1,124 @@
import classNames from "classnames";
import React from "react";
import Swal from "sweetalert2";
import { deleteFile, renameFile } from "../../api/filesAPI";
import { useFiles, useQuickFiles } from "../../hooks/files";
class NewContextMenu extends React.Component {
constructor(props) {
super(props);
}
const NewContextMenu = (props) => {
const { invalidateFilesCache } = useFiles();
const { invalidateQuickFilesCache } = useQuickFiles();
const liClassname =
"flex w-full px-[20px] py-[12px] items-center font-normal justify-start no-underline transition-all duration-400 ease-in-out hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium";
const spanClassname = "inline-flex mr-[18px]";
render() {
return (
// settings__folder__margin
<div
onClick={this.props.stopPropagation}
ref={this.props.wrapperRef}
className={classNames(
"fixed min-w-[215px] bg-white shadow-[0px_2px_4px_rgba(0,0,0,0.15),_inset_0px_1px_0px_#f5f7fa] rounded-[4px] mt-[-5px] z-[2] animate-very-long",
this.props.contextSelected ? "opacity-100" : "opacity-0"
)}
style={
this.props.contextSelected
? {
display: "block",
left: `${this.props.contextSelected.width}px`,
top: `${this.props.contextSelected.height}px`,
}
: { display: "none" }
}
>
<ul className="p-0 list-none m-0 ">
<li className="flex w-full px-[20px] py-[12px] items-center font-normal justify-start no-underline transition-all duration-400 ease-in-out hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium">
<a onClick={this.props.startRenameFile} className="flex">
<span className="inline-flex mr-[18px]">
<img src="/assets/filesetting1.svg" alt="setting" />
</span>
Rename
</a>
</li>
{!this.props.folderMode ? (
<li className="flex w-full px-[20px] py-[12px] items-center font-normal justify-start no-underline transition-all duration-400 ease-in-out hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium">
<a
onClick={this.props.startShareFile}
className="flex"
data-modal="share__modal"
>
<span
className="inline-flex mr-[18px]
const renameItem = async () => {
if (!props.folderMode) {
const { value: filename } = await Swal.fire({
title: "Enter A File Name",
input: "text",
inputValue: props.file.filename,
showCancelButton: true,
inputValidator: (value) => {
if (!value) {
return "Please Enter a Name";
}
},
});
await renameFile(props.file._id, filename);
invalidateFilesCache();
invalidateQuickFilesCache();
}
};
const deleteItem = async () => {
if (!props.folderMode) {
const result = await Swal.fire({
title: "Confirm Deletion",
text: "You cannot undo this action",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete",
});
if (result.value) {
await deleteFile(props.file._id);
invalidateFilesCache();
invalidateQuickFilesCache();
}
}
};
return (
<div
onClick={props.stopPropagation}
ref={props.wrapperRef}
className={classNames(
"fixed min-w-[215px] bg-white shadow-[0px_2px_4px_rgba(0,0,0,0.15),_inset_0px_1px_0px_#f5f7fa] rounded-[4px] mt-[-5px] z-[2] animate-very-long",
props.contextSelected.selected ? "opacity-100" : "opacity-0"
)}
style={
props.contextSelected.selected
? {
display: "block",
left: `${props.contextSelected.X}px`,
top: `${props.contextSelected.Y}px`,
}
: { display: "none" }
}
>
<ul className="p-0 list-none m-0 ">
<li onClick={renameItem} className={liClassname}>
<a className="flex">
<span className={spanClassname}>
<img src="/assets/filesetting1.svg" alt="setting" />
</span>
Rename
</a>
</li>
{!props.folderMode ? (
<li onClick={props.startShareFile} className={liClassname}>
<a className="flex" data-modal="share__modal">
<span
className="inline-flex mr-[18px]
"
>
<img src="/assets/filesetting2.svg" alt="setting" />
</span>
Share
</a>
</li>
) : undefined}
{!this.props.folderMode ? (
<li className="flex w-full px-[20px] py-[12px] items-center font-normal justify-start no-underline transition-all duration-400 ease-in-out hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium">
<a onClick={this.props.startFileDownload} className="flex">
<span className="inline-flex mr-[18px]">
<img src="/assets/filesetting3.svg" alt="setting" />
</span>
Download
</a>
</li>
) : undefined}
<li className="flex w-full px-[20px] py-[12px] items-center font-normal justify-start no-underline transition-all duration-400 ease-in-out hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium">
<a
onClick={this.props.startMovingFile}
className="flex"
data-modal="destination__modal"
>
<span className="inline-flex mr-[18px]">
<img src="/assets/filesetting4.svg" alt="setting" />
</span>{" "}
Move
</a>
</li>
<li className="flex w-full px-[20px] py-[12px] items-center font-normal justify-start no-underline transition-all duration-400 ease-in-out hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium">
<a onClick={this.props.startDeleteFile} className="flex">
<span className="inline-flex mr-[18px]">
<img src="/assets/filesetting5.svg" alt="setting" />
>
<img src="/assets/filesetting2.svg" alt="setting" />
</span>
Delete
Share
</a>
</li>
</ul>
</div>
);
}
}
) : undefined}
{!props.folderMode ? (
<li onClick={props.startFileDownload} className={liClassname}>
<a className="flex">
<span className={spanClassname}>
<img src="/assets/filesetting3.svg" alt="setting" />
</span>
Download
</a>
</li>
) : undefined}
<li onClick={props.startMovingFile} className={liClassname}>
<a className="flex" data-modal="destination__modal">
<span className={spanClassname}>
<img src="/assets/filesetting4.svg" alt="setting" />
</span>{" "}
Move
</a>
</li>
<li onClick={deleteItem} className={liClassname}>
<a className="flex">
<span className={spanClassname}>
<img src="/assets/filesetting5.svg" alt="setting" />
</span>
Delete
</a>
</li>
</ul>
</div>
);
};
export default NewContextMenu;
+57 -57
View File
@@ -1,73 +1,73 @@
import React from "react";
import {connect} from "react-redux";
import { connect } from "react-redux";
import { setShareSelected } from "../../actions/selectedItem";
import NewContextMenu from "./NewContextMenu";
class NewContextMenuContainer extends React.Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.wrapperRef = React.createRef();
}
this.wrapperRef = React.createRef();
componentDidMount = () => {
document.addEventListener("mousedown", this.handleClickOutside);
};
componentWillUnmount = () => {
document.removeEventListener("mousedown", this.handleClickOutside);
};
componentDidUpdate = () => {};
handleClickOutside = () => {
if (
this.props.contextSelected.selected &&
this.wrapperRef &&
!this.wrapperRef.current.contains(event.target)
) {
this.props.closeContext();
}
};
componentDidMount = () => {
startFileDownload = () => {
console.log("start download file", this.props.file._id);
this.props.downloadFile(this.props.file._id, this.props.file);
};
document.addEventListener("mousedown", this.handleClickOutside)
}
startRenameFile = async (e) => {
console.log("rename file click");
this.props.changeEditNameMode();
};
componentWillUnmount = () => {
startDeleteFile = async () => {
this.props.changeDeleteMode();
};
document.removeEventListener("mousedown", this.handleClickOutside)
}
startShareFile = () => {
this.props.dispatch(setShareSelected({ ...this.props.file }));
};
componentDidUpdate = () => {
}
handleClickOutside = () => {
if (this.props.contextSelected && this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
this.props.closeContext();
}
}
startFileDownload = () => {
console.log("start download file", this.props.file._id);
this.props.downloadFile(this.props.file._id, this.props.file)
}
startRenameFile = async(e) => {
console.log("rename file click")
this.props.changeEditNameMode();
}
startDeleteFile = async() => {
this.props.changeDeleteMode()
}
startShareFile = () => {
this.props.dispatch(setShareSelected({...this.props.file}))
}
stopPropagation = (e) => {
e.stopPropagation()
if (this.props.quickItemMode || this.props.gridMode) this.props.closeContext();
}
render() {
return <NewContextMenu
wrapperRef={this.wrapperRef}
stopPropagation={this.stopPropagation}
startRenameFile={this.startRenameFile}
startShareFile={this.startShareFile}
startFileDownload={this.startFileDownload}
startDeleteFile={this.startDeleteFile}
state={this.state}
{...this.props}
/>
}
stopPropagation = (e) => {
e.stopPropagation();
if (this.props.quickItemMode || this.props.gridMode)
this.props.closeContext();
};
render() {
return (
<NewContextMenu
wrapperRef={this.wrapperRef}
stopPropagation={this.stopPropagation}
startRenameFile={this.startRenameFile}
startShareFile={this.startShareFile}
startFileDownload={this.startFileDownload}
startDeleteFile={this.startDeleteFile}
state={this.state}
{...this.props}
/>
);
}
}
export default connect()(NewContextMenuContainer);
export default connect()(NewContextMenuContainer);
@@ -1,117 +1,79 @@
import capitalize from "../../utils/capitalize";
import moment from "moment";
import React, { useMemo } from "react";
import React, { useMemo, useState } from "react";
import NewContextMenu from "../NewContextMenu";
import classNames from "classnames";
import { getFileColor, getFileExtension } from "../../utils/files";
import { useThumbnail } from "../../hooks/files";
import { useContextMenu } from "../../hooks/contextMenu";
const QuickAccessItem = (props) => {
const fileExtension = useMemo(() => {
const filenameSplit = props.filename.split(".");
const { image, hasThumbnail, imageOnError } = useThumbnail(
props.metadata.hasThumbnail,
props.metadata.thumbnailID
);
if (filenameSplit.length > 1) {
let extension = filenameSplit[filenameSplit.length - 1];
const { onContextMenu, closeContextMenu, ...contextMenuState } =
useContextMenu();
if (extension.length > 4)
extension =
extension.substring(0, 3) +
extension.substring(extension.length - 1, extension.length);
const fileExtension = useMemo(
() => getFileExtension(props.filename),
[props.filename]
);
return extension.toUpperCase();
} else {
return "UNK";
}
}, [props.filename]);
const imageColor = useMemo(() => {
const letter = fileExtension.substring(0, 1).toUpperCase();
const colorObj = {
A: "#e53935",
B: "#d81b60",
C: "#8e24aa",
D: "#5e35b1",
E: "#3949ab",
F: "#1e88e5",
G: "#039be5",
H: "#00acc1",
I: "#00897b",
J: "#43a047",
K: "#fdd835",
L: "#ffb300",
M: "#fb8c00",
N: "#f4511e",
O: "#d32f2f",
P: "#c2185b",
Q: "#7b1fa2",
R: "#512da8",
S: "#303f9f",
T: "#1976d2",
U: "#0288d1",
V: "#0097a7",
W: "#0097a7",
X: "#00796b",
Y: "#388e3c",
Z: "#fbc02d",
};
if (colorObj[letter]) {
return colorObj[letter];
} else {
return "#03a9f4";
}
}, [props.filename]);
const imageColor = useMemo(
() => getFileColor(props.filename),
[props.filename]
);
const elementSelected = useMemo(
() => `quick-${props._id}` === props.selected,
[props._id, props.selected]
);
console.log("thumbnail", props.state);
return (
<div
className={classNames(
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer w-48 flex items-center justify-center flex-col h-[150px] animiate hover:border-[#3c85ee] overflow-hidden",
{
"border-[#3c85ee]": elementSelected,
"border-[#ebe9f9]": !elementSelected,
}
elementSelected ? "border-[#3c85ee]" : "border-[#ebe9f9]"
)}
onClick={() => {
props.fileClick(props._id, props, true);
}}
onContextMenu={props.selectContext}
onContextMenu={onContextMenu}
onTouchStart={props.onTouchStart}
onTouchEnd={props.onTouchEnd}
onTouchMove={props.onTouchMove}
>
<div onClick={props.clickStopPropagation}>
<NewContextMenu
gridMode={true}
quickItemMode={true}
contextSelected={props.state.contextSelected}
closeContext={props.closeContext}
downloadFile={props.downloadFile}
file={props}
changeEditNameMode={props.changeEditNameMode}
closeEditNameMode={props.closeEditNameMode}
changeDeleteMode={props.changeDeleteMode}
startMovingFile={props.startMovingFile}
/>
</div>
{contextMenuState.selected && (
<div onClick={props.clickStopPropagation}>
<NewContextMenu
gridMode={true}
quickItemMode={true}
contextSelected={contextMenuState}
closeContext={closeContextMenu}
downloadFile={props.downloadFile}
file={props}
changeEditNameMode={props.changeEditNameMode}
closeEditNameMode={props.closeEditNameMode}
changeDeleteMode={props.changeDeleteMode}
startMovingFile={props.startMovingFile}
/>
</div>
)}
<div
className={classNames(
"inline-flex items-center w-full bg-white relative",
{
"mt-2": !props.state.hasThumbnail,
"mt-2": !hasThumbnail,
}
)}
>
{props.state.hasThumbnail ? (
{hasThumbnail ? (
<img
className="w-full min-h-[88px] max-h-[88px] h-full flex object-cover"
src={props.state.image}
onError={props.thumbnailOnError}
src={image}
onError={imageOnError}
/>
) : (
<svg
@@ -129,7 +91,7 @@ const QuickAccessItem = (props) => {
/>
</svg>
)}
{!props.state.hasThumbnail && (
{!hasThumbnail && (
<div className="w-full h-full absolute flex justify-center items-center text-white mt-3">
<p className="text-sm">{fileExtension}</p>
</div>
+1 -132
View File
@@ -25,7 +25,6 @@ class QuickAccessItemContainer extends React.Component {
this.lastTouch = 0;
this.state = {
contextMenuPos: {},
image: "/images/file-svg.svg",
imageClassname: "noSelect file__item-no-thumbnail",
contextSelected: false,
@@ -33,99 +32,6 @@ class QuickAccessItemContainer extends React.Component {
};
}
componentDidMount = () => {
const hasThumbnail = this.props.metadata.hasThumbnail;
console.log("has", hasThumbnail);
if (hasThumbnail && !this.failedToLoad) {
this.getThumbnail();
}
};
closeContext = () => {
this.setState(() => {
return {
...this.state,
contextSelected: false,
};
});
};
selectContext = (e) => {
if (e) e.stopPropagation();
if (e) e.preventDefault();
if (mobilecheck()) {
this.props.dispatch(setMobileContextMenu(true, this.props));
return;
}
this.setState(() => {
return {
...this.state,
contextSelected: {
width: e.clientX,
height: e.clientY,
},
};
});
};
getThumbnail = async () => {
const thumbnailID = this.props.metadata.thumbnailID;
const imageClassname = "noSelect";
// GOOGLE DRIVE IMAGE
if (this.props.metadata.drive) {
return await this.setState(() => ({
...this.state,
image: this.props.metadata.thumbnailID,
imageClassname: imageClassname,
}));
}
const config = {
responseType: "arraybuffer",
};
await this.setState(() => ({
...this.state,
image: "/images/file-svg.svg",
imageClassname: "noSelect file__item-no-thumbnail",
}));
const url = `http://localhost:5173/api/file-service/thumbnail/${thumbnailID}`;
axios
.get(url, config)
.then((results) => {
const imgFile = new Blob([results.data]);
const imgUrl = URL.createObjectURL(imgFile);
console.log("got image");
this.setState(() => ({
...this.state,
image: imgUrl,
imageClassname: imageClassname,
hasThumbnail: true,
}));
})
.catch((err) => {
console.log(err);
});
};
thumbnailOnError = (e) => {
console.log("thumbnail on error", e);
this.setState(() => ({
...this.state,
image: "/images/file-svg.svg",
imageClassname: "noSelect file__item-no-thumbnail",
hasThumbnail: false,
}));
};
onTouchStart = () => {
const date = new Date();
this.lastTouch = date.getTime();
@@ -146,43 +52,10 @@ class QuickAccessItemContainer extends React.Component {
this.lastTouch = 0;
if (difference > 500) {
this.selectContext();
// this.selectContext();
}
};
getContextMenu = (e) => {
if (e) e.preventDefault();
const isMobile = mobileCheck();
const windowX = window.innerWidth;
const windowY = window.innerHeight;
let styleObj = { right: 0, left: 0, top: "-3px", bottom: 0 };
if (isMobile) {
styleObj = { bottom: 0, left: "2px" };
} else {
const clientY = e.nativeEvent.clientY;
const clientX = e.nativeEvent.clientX;
if (clientX > windowX / 2) {
styleObj = { ...styleObj, left: "unset", right: 0 };
} else {
styleObj = { ...styleObj, left: 0, right: "unset" };
}
}
this.setState(() => ({
...this.state,
contextMenuPos: styleObj,
}));
this.props.dispatch(setSelected("quick-" + this.props._id));
this.props.dispatch(setRightSelected("quick-" + this.props._id));
this.props.dispatch(setLastSelected(0));
};
changeEditNameMode = async () => {
let inputValue = this.props.filename;
@@ -257,17 +130,13 @@ class QuickAccessItemContainer extends React.Component {
render() {
return (
<QuickAccessItem
getContextMenu={this.getContextMenu}
onTouchStart={this.onTouchStart}
onTouchMove={this.onTouchMove}
onTouchEnd={this.onTouchEnd}
closeContext={this.closeContext}
selectContext={this.selectContext}
changeEditNameMode={this.changeEditNameMode}
closeEditNameMode={this.closeEditNameMode}
changeDeleteMode={this.changeDeleteMode}
startMovingFile={this.startMovingFile}
thumbnailOnError={this.thumbnailOnError}
state={this.state}
{...this.props}
/>
+33
View File
@@ -0,0 +1,33 @@
import { useState } from "react";
export const useContextMenu = () => {
const [contextData, setContextData] = useState({
selected: false,
X: 0,
Y: 0,
lastTouched: 0,
});
const onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
if (e) e.stopPropagation();
if (e) e.preventDefault();
setContextData({
...contextData,
selected: true,
X: e.clientX,
Y: e.clientY,
});
};
const closeContextMenu = () => {
setContextData({
...contextData,
selected: false,
X: 0,
Y: 0,
lastTouched: 0,
});
};
return { ...contextData, onContextMenu, closeContextMenu };
};
+74 -4
View File
@@ -1,6 +1,11 @@
import { useInfiniteQuery, useQuery } from "react-query";
import { useInfiniteQuery, useQuery, useQueryClient } from "react-query";
import { useParams } from "react-router-dom";
import { getFilesList, getQuickFilesList } from "../api/filesAPI";
import {
getFileThumbnail,
getFilesList,
getQuickFilesList,
} from "../api/filesAPI";
import { useCallback, useEffect, useState } from "react";
export const useFiles = () => {
const params = useParams();
@@ -17,6 +22,7 @@ export const useFiles = () => {
getFilesList,
{
getNextPageParam: (lastPage, pages) => {
console.log("pages", pages);
const lastElement = lastPage[lastPage.length - 1];
if (!lastElement) return undefined;
return {
@@ -27,15 +33,79 @@ export const useFiles = () => {
}
);
const filesReactClientQuery = useQueryClient();
const invalidateFilesCache = () => {
filesReactClientQuery.invalidateQueries({
queryKey: [
"files",
{
parent: params.id || "/",
search: "",
sortBy: undefined,
limit: undefined,
},
],
});
};
const testFunction = () => {
console.log("this is a test function");
};
return { ...filesReactQuery, testFunction };
return { ...filesReactQuery, testFunction, invalidateFilesCache };
};
export const useQuickFiles = () => {
const quickFilesQuery = useQuery("quickFiles", getQuickFilesList);
return quickFilesQuery;
const quickFilesReactClientQuery = useQueryClient();
const invalidateQuickFilesCache = () => {
quickFilesReactClientQuery.invalidateQueries({
queryKey: "quickFiles",
});
};
return { ...quickFilesQuery, invalidateQuickFilesCache };
};
interface thumbnailState {
hasThumbnail: boolean;
image: null | string;
}
export const useThumbnail = (hasThumbnail: boolean, thumbnailID: string) => {
const [state, setState] = useState<thumbnailState>({
hasThumbnail: false,
image: null,
});
const getThumbnail = useCallback(async () => {
try {
const thumbnailData = await getFileThumbnail(thumbnailID);
setState({
hasThumbnail: true,
image: thumbnailData,
});
} catch (e) {
console.log("error getting thumbnail data", e);
imageOnError();
}
}, [thumbnailID]);
const imageOnError = () => {
setState({
hasThumbnail: false,
image: null,
});
};
useEffect(() => {
if (!hasThumbnail) return;
getThumbnail();
}, [hasThumbnail, getThumbnail]);
return { ...state, imageOnError };
};
+59
View File
@@ -0,0 +1,59 @@
export const getFileExtension = (filename: string) => {
const filenameSplit = filename.split(".");
if (filenameSplit.length > 1) {
let extension = filenameSplit[filenameSplit.length - 1];
if (extension.length > 4)
extension =
extension.substring(0, 3) +
extension.substring(extension.length - 1, extension.length);
return extension.toUpperCase();
} else {
return "UNK";
}
};
type ColorMap = {
[key: string]: string;
};
export const getFileColor = (filename: string) => {
const letter = getFileExtension(filename).substring(0, 1).toUpperCase();
const colorMap: ColorMap = {
A: "#e53935",
B: "#d81b60",
C: "#8e24aa",
D: "#5e35b1",
E: "#3949ab",
F: "#1e88e5",
G: "#039be5",
H: "#00acc1",
I: "#00897b",
J: "#43a047",
K: "#fdd835",
L: "#ffb300",
M: "#fb8c00",
N: "#f4511e",
O: "#d32f2f",
P: "#c2185b",
Q: "#7b1fa2",
R: "#512da8",
S: "#303f9f",
T: "#1976d2",
U: "#0288d1",
V: "#0097a7",
W: "#0097a7",
X: "#00796b",
Y: "#388e3c",
Z: "#fbc02d",
};
if (colorMap[letter]) {
return colorMap[letter];
} else {
return "#03a9f4";
}
};