mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-10-05 18:38:06 +03:00
feat(frontend): better UI for General Settings (#2773)
This commit is contained in:
parent
bbbf0d1217
commit
41f120bc07
@ -1,108 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useApiKeyConfig } from "../useApiKeyConfig";
|
||||
|
||||
const createApiKeyMock = vi.fn(() => "dummyApiKey");
|
||||
const trackMock = vi.fn((props: unknown) => ({ props }));
|
||||
|
||||
const mockUseSupabase = vi.fn(() => ({
|
||||
session: {
|
||||
user: {},
|
||||
},
|
||||
}));
|
||||
|
||||
const useAuthApiMock = vi.fn(() => ({
|
||||
createApiKey: () => createApiKeyMock(),
|
||||
}));
|
||||
|
||||
const useEventTrackingMock = vi.fn(() => ({
|
||||
track: (props: unknown) => trackMock(props),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/auth/useAuthApi", () => ({
|
||||
useAuthApi: () => useAuthApiMock(),
|
||||
}));
|
||||
vi.mock("@/services/analytics/june/useEventTracking", () => ({
|
||||
useEventTracking: () => useEventTrackingMock(),
|
||||
}));
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/hooks", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/hooks")>(
|
||||
"@/lib/hooks"
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
put: vi.fn(() => ({})),
|
||||
get: vi.fn(() => ({})),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@tanstack/react-query", async () => {
|
||||
const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
|
||||
"@tanstack/react-query"
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useQuery: () => ({
|
||||
data: {},
|
||||
}),
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: vi.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
describe("useApiKeyConfig", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should set the apiKey when handleCreateClick is called", async () => {
|
||||
const { result } = renderHook(() => useApiKeyConfig());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCreateClick();
|
||||
});
|
||||
|
||||
expect(createApiKeyMock).toHaveBeenCalledTimes(1);
|
||||
expect(trackMock).toHaveBeenCalledWith("CREATE_API_KEY");
|
||||
expect(result.current.apiKey).toBe("dummyApiKey");
|
||||
});
|
||||
|
||||
it("should call copyToClipboard when handleCopyClick is called with a non-empty apiKey", () => {
|
||||
vi.mock("react", async () => {
|
||||
const actual = await vi.importActual<typeof import("react")>("react");
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useState: () => ["dummyApiKey", vi.fn()],
|
||||
};
|
||||
});
|
||||
//@ts-ignore - clipboard is not actually readonly
|
||||
global.navigator.clipboard = {
|
||||
writeText: vi.fn(),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useApiKeyConfig());
|
||||
|
||||
act(() => result.current.handleCopyClick());
|
||||
|
||||
expect(trackMock).toHaveBeenCalledTimes(1);
|
||||
expect(trackMock).toHaveBeenCalledWith("COPY_API_KEY");
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(global.navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
"dummyApiKey"
|
||||
);
|
||||
});
|
||||
});
|
@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { CountrySelector } from "@/lib/components/ui/CountrySelector/CountrySelector";
|
||||
|
||||
import { useLanguageHook } from "./hooks/useLanguageHook";
|
||||
|
||||
const LanguageSelect = (): JSX.Element => {
|
||||
const { t } = useTranslation(["translation"]);
|
||||
const { currentLanguage, change } = useLanguageHook();
|
||||
|
||||
if (!currentLanguage) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CountrySelector
|
||||
iconName="flag"
|
||||
label={t("languageSelect")}
|
||||
currentValue={currentLanguage}
|
||||
setCurrentValue={change}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageSelect.displayName = "LanguageSelect";
|
||||
|
||||
export default LanguageSelect;
|
@ -1,87 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
export type Language = {
|
||||
label: string;
|
||||
flag: string;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
label: "English",
|
||||
flag: "🇬🇧",
|
||||
shortName: "en",
|
||||
},
|
||||
{
|
||||
label: "Español",
|
||||
flag: "🇪🇸",
|
||||
shortName: "es",
|
||||
},
|
||||
{
|
||||
label: "Français",
|
||||
flag: "🇫🇷",
|
||||
shortName: "fr",
|
||||
},
|
||||
{
|
||||
label: "Português",
|
||||
flag: "🇵🇹",
|
||||
shortName: "pt",
|
||||
},
|
||||
{
|
||||
label: "Русский",
|
||||
flag: "🇷🇺",
|
||||
shortName: "ru",
|
||||
},
|
||||
{
|
||||
label: "简体中文",
|
||||
flag: "🇨🇳",
|
||||
shortName: "zh",
|
||||
},
|
||||
];
|
||||
|
||||
export const useLanguageHook = (): {
|
||||
change: (newLanguage: Language) => void;
|
||||
allLanguages: Language[];
|
||||
currentLanguage: Language | undefined;
|
||||
} => {
|
||||
const { i18n } = useTranslation();
|
||||
const [allLanguages, setAllLanguages] = useState<Language[]>([]);
|
||||
const [currentLanguage, setCurrentLanguage] = useState<Language>();
|
||||
const { track } = useEventTracking();
|
||||
|
||||
useEffect(() => {
|
||||
setAllLanguages(languages);
|
||||
const savedLanguage = localStorage.getItem("selectedLanguage") ?? "English";
|
||||
let choosedLanguage = languages.find(
|
||||
(lang) => lang.label === savedLanguage
|
||||
);
|
||||
if (!choosedLanguage) {
|
||||
choosedLanguage = languages.find((lang) => lang.label === "English");
|
||||
}
|
||||
if (currentLanguage) {
|
||||
setCurrentLanguage(choosedLanguage);
|
||||
localStorage.setItem("selectedLanguage", currentLanguage.label);
|
||||
void i18n.changeLanguage(currentLanguage.shortName);
|
||||
} else {
|
||||
console.error(
|
||||
"No valid language found, please check the languages configuration."
|
||||
);
|
||||
}
|
||||
}, [i18n]);
|
||||
|
||||
const change = (newLanguage: Language) => {
|
||||
void track("CHANGE_LANGUAGE");
|
||||
setCurrentLanguage(newLanguage);
|
||||
localStorage.setItem("selectedLanguage", newLanguage.label);
|
||||
void i18n.changeLanguage(newLanguage.shortName);
|
||||
};
|
||||
|
||||
return {
|
||||
change,
|
||||
allLanguages,
|
||||
currentLanguage,
|
||||
};
|
||||
};
|
@ -1,30 +1,25 @@
|
||||
/* eslint-disable max-lines */
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { CopyButton } from "@/lib/components/ui/CopyButton";
|
||||
import { FieldHeader } from "@/lib/components/ui/FieldHeader/FieldHeader";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
|
||||
import styles from "./ApiKeyConfig.module.scss";
|
||||
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
|
||||
|
||||
export const ApiKeyConfig = (): JSX.Element => {
|
||||
const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
|
||||
const { t } = useTranslation(["config"]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldHeader iconName="key" label={`Quivr ${t("apiKey")}`} />
|
||||
{apiKey === "" ? (
|
||||
<Button
|
||||
data-testid="create-new-key"
|
||||
variant="secondary"
|
||||
onClick={() => void handleCreateClick()}
|
||||
>
|
||||
Create New Key
|
||||
</Button>
|
||||
<QuivrButton
|
||||
iconName="key"
|
||||
color="primary"
|
||||
label="Create new key"
|
||||
onClick={void handleCreateClick()}
|
||||
small={true}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.response_wrapper}>
|
||||
<span>{apiKey}</span>
|
@ -0,0 +1,30 @@
|
||||
@use "styles/Spacings.module.scss";
|
||||
@use "styles/Typography.module.scss";
|
||||
|
||||
.info_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
border-bottom: 1px solid var(--border-2);
|
||||
padding-block: Spacings.$spacing04;
|
||||
position: relative;
|
||||
font-size: Typography.$small;
|
||||
|
||||
&.without_border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.title_wrapper {
|
||||
display: flex;
|
||||
margin-top: 0;
|
||||
gap: Spacings.$spacing03;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
|
||||
.title {
|
||||
min-width: 140px;
|
||||
max-width: 140px;
|
||||
color: var(--text-4);
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./InfoSection.module.scss";
|
||||
|
||||
interface InfoSectionProps {
|
||||
iconName: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
export const InfoSection = ({
|
||||
iconName,
|
||||
title,
|
||||
children,
|
||||
last,
|
||||
}: InfoSectionProps): JSX.Element => (
|
||||
<div
|
||||
className={`${styles.info_wrapper} ${last ? styles.without_border : ""}`}
|
||||
>
|
||||
<div className={styles.title_wrapper}>
|
||||
<Icon name={iconName} color="grey" size="small" />
|
||||
<span className={styles.title}>{title}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
@ -8,8 +8,52 @@
|
||||
gap: Spacings.$spacing07;
|
||||
width: auto;
|
||||
padding: Spacings.$spacing06;
|
||||
max-width: 700px;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
|
||||
.infos_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.bold {
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.credits {
|
||||
color: var(--gold);
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.remaining_credits {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text_and_button {
|
||||
font-size: Typography.$small;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing04;
|
||||
|
||||
.text {
|
||||
.link {
|
||||
font-weight: 550;
|
||||
color: var(--primary-0);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,73 @@
|
||||
import { InfoDisplayer } from "@/lib/components/ui/InfoDisplayer/InfoDisplayer";
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import { ApiKeyConfig } from "./ApiKeyConfig";
|
||||
import { InfoSection } from "./InfoSection/InfoSection";
|
||||
import styles from "./Settings.module.scss";
|
||||
|
||||
import { ApiKeyConfig } from "../ApiKeyConfig";
|
||||
import LanguageSelect from "../LanguageSelect/LanguageSelect";
|
||||
import { StripePricingOrManageButton } from "../StripePricingOrManageButton";
|
||||
|
||||
type InfoDisplayerProps = {
|
||||
email: string;
|
||||
username: string;
|
||||
remainingCredits: number;
|
||||
};
|
||||
|
||||
export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => {
|
||||
export const Settings = ({
|
||||
email,
|
||||
username,
|
||||
remainingCredits,
|
||||
}: InfoDisplayerProps): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.settings_wrapper}>
|
||||
<span className={styles.title}>
|
||||
General settings and main information
|
||||
</span>
|
||||
<InfoDisplayer label="Email" iconName="email">
|
||||
<span>{email}</span>
|
||||
</InfoDisplayer>
|
||||
<LanguageSelect />
|
||||
<ApiKeyConfig />
|
||||
<StripePricingOrManageButton />
|
||||
<div className={styles.infos_wrapper}>
|
||||
<InfoSection iconName="email" title="Email">
|
||||
<span className={styles.bold}>{email}</span>
|
||||
</InfoSection>
|
||||
<InfoSection iconName="user" title="Username">
|
||||
<span className={styles.bold}>{username}</span>
|
||||
</InfoSection>
|
||||
<InfoSection iconName="coin" title="Remaining credits">
|
||||
<div className={styles.remaining_credits}>
|
||||
<span className={styles.credits}>{remainingCredits}</span>
|
||||
<Icon name="coin" color="gold" size="normal" />
|
||||
</div>
|
||||
</InfoSection>
|
||||
<InfoSection iconName="key" title="Quivr API Key">
|
||||
<div className={styles.text_and_button}>
|
||||
<span className={styles.text}>
|
||||
The Quivr API key is a unique identifier that allows you to access
|
||||
and interact with{" "}
|
||||
<a
|
||||
href="https://api.quivr.app/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Quivr's API.
|
||||
</a>
|
||||
</span>
|
||||
<div className={styles.button}>
|
||||
<ApiKeyConfig />
|
||||
</div>
|
||||
</div>
|
||||
</InfoSection>
|
||||
<InfoSection iconName="star" title="My plan" last={true}>
|
||||
<div className={styles.text_and_button}>
|
||||
<span className={styles.text}>
|
||||
Customize your subscription to best suit your needs. By upgrading
|
||||
to a premium plan, you gain access to a host of additional
|
||||
benefits, including significantly more chat credits and the
|
||||
ability to create more Brains.
|
||||
</span>
|
||||
<div className={styles.button}>
|
||||
<StripePricingOrManageButton small={true} />
|
||||
</div>
|
||||
</div>
|
||||
</InfoSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,13 @@ import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
const MANAGE_PLAN_URL = process.env.NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL;
|
||||
|
||||
export const StripePricingOrManageButton = (): JSX.Element => {
|
||||
type StripePricingModalButtonProps = {
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
export const StripePricingOrManageButton = ({
|
||||
small = false,
|
||||
}: StripePricingModalButtonProps): JSX.Element => {
|
||||
const { userData } = useUserData();
|
||||
|
||||
const is_premium = userData?.is_premium ?? false;
|
||||
@ -15,6 +21,7 @@ export const StripePricingOrManageButton = (): JSX.Element => {
|
||||
label="Manage my plan"
|
||||
color="gold"
|
||||
iconName="star"
|
||||
small={small}
|
||||
></QuivrButton>
|
||||
</a>
|
||||
);
|
||||
@ -28,6 +35,7 @@ export const StripePricingOrManageButton = (): JSX.Element => {
|
||||
label="Upgrade my plan"
|
||||
color="gold"
|
||||
iconName="star"
|
||||
small={small}
|
||||
></QuivrButton>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
@ -9,6 +9,7 @@ import { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { ButtonType } from "@/lib/types/QuivrButton";
|
||||
@ -22,8 +23,8 @@ import { useLogoutModal } from "../../lib/hooks/useLogoutModal";
|
||||
|
||||
const UserPage = (): JSX.Element => {
|
||||
const { session } = useSupabase();
|
||||
const { userData } = useUserData();
|
||||
const { deleteUserData } = useUserApi();
|
||||
const { userData, userIdentityData } = useUserData();
|
||||
const { deleteUserData, getUserCredits } = useUserApi();
|
||||
const { t } = useTranslation(["translation", "logout"]);
|
||||
const [deleteAccountModalOpened, setDeleteAccountModalOpened] =
|
||||
useState(false);
|
||||
@ -34,6 +35,14 @@ const UserPage = (): JSX.Element => {
|
||||
setIsLogoutModalOpened,
|
||||
} = useLogoutModal();
|
||||
const [selectedTab, setSelectedTab] = useState("Connections");
|
||||
const { remainingCredits, setRemainingCredits } = useUserSettingsContext();
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const res = await getUserCredits();
|
||||
setRemainingCredits(res);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const buttons: ButtonType[] = [
|
||||
{
|
||||
@ -82,7 +91,13 @@ const UserPage = (): JSX.Element => {
|
||||
<Tabs tabList={studioTabs} />
|
||||
<div className={styles.user_page_menu}></div>
|
||||
<div className={styles.content_wrapper}>
|
||||
{selectedTab === "General" && <Settings email={userData.email} />}
|
||||
{selectedTab === "General" && (
|
||||
<Settings
|
||||
email={userData.email}
|
||||
username={userIdentityData?.username ?? ""}
|
||||
remainingCredits={remainingCredits ?? 0}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === "Connections" && <Connections />}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,17 +0,0 @@
|
||||
@use "styles/Radius.module.scss";
|
||||
|
||||
.selection {
|
||||
border-radius: Radius.$normal;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
background-color: var(--background-0);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-3);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
border-color: var(--primary-0);
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import {
|
||||
Language,
|
||||
useLanguageHook,
|
||||
} from "@/app/user/components/LanguageSelect/hooks/useLanguageHook";
|
||||
|
||||
import styles from "./CountrySelector.module.scss";
|
||||
|
||||
import { FieldHeader } from "../FieldHeader/FieldHeader";
|
||||
|
||||
type CountrySelectorProps = {
|
||||
iconName: string;
|
||||
label: string;
|
||||
currentValue: { label: string; flag: string };
|
||||
setCurrentValue: (newCountry: Language) => void;
|
||||
};
|
||||
|
||||
export const CountrySelector = ({
|
||||
iconName,
|
||||
label,
|
||||
currentValue,
|
||||
setCurrentValue,
|
||||
}: CountrySelectorProps): JSX.Element => {
|
||||
const { allLanguages } = useLanguageHook();
|
||||
|
||||
return (
|
||||
<div className={styles.country_selector_container}>
|
||||
<FieldHeader iconName={iconName} label={label} />
|
||||
<select
|
||||
className={styles.selection}
|
||||
data-testid="language-select"
|
||||
name="language"
|
||||
id="language"
|
||||
value={currentValue.label}
|
||||
onChange={(e) =>
|
||||
setCurrentValue(
|
||||
allLanguages.find(
|
||||
(language) => language.label === e.target.value
|
||||
) ?? allLanguages[2]
|
||||
)
|
||||
}
|
||||
>
|
||||
{allLanguages.map((lang) => (
|
||||
<option
|
||||
data-testid={`option-${lang}`}
|
||||
value={lang.label}
|
||||
key={lang.shortName}
|
||||
>
|
||||
{lang.flag} {lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -82,4 +82,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ import {
|
||||
FaFileAlt,
|
||||
FaFolder,
|
||||
FaGithub,
|
||||
FaKey,
|
||||
FaLinkedin,
|
||||
FaQuestionCircle,
|
||||
FaRegFileAlt,
|
||||
@ -79,7 +78,6 @@ import {
|
||||
LuSearch,
|
||||
} from "react-icons/lu";
|
||||
import {
|
||||
MdAlternateEmail,
|
||||
MdDarkMode,
|
||||
MdDashboardCustomize,
|
||||
MdDeleteOutline,
|
||||
@ -89,7 +87,9 @@ import {
|
||||
MdMarkEmailRead,
|
||||
MdMarkEmailUnread,
|
||||
MdOutlineLightMode,
|
||||
MdOutlineMail,
|
||||
MdOutlineModeEditOutline,
|
||||
MdOutlineVpnKey,
|
||||
MdUnfoldLess,
|
||||
MdUnfoldMore,
|
||||
MdUploadFile,
|
||||
@ -125,7 +125,7 @@ export const iconList: { [name: string]: IconType } = {
|
||||
docx: BsFiletypeDocx,
|
||||
download: IoCloudDownloadOutline,
|
||||
edit: MdOutlineModeEditOutline,
|
||||
email: MdAlternateEmail,
|
||||
email: MdOutlineMail,
|
||||
epub: FaFile,
|
||||
eureka: GoLightBulb,
|
||||
externLink: LuExternalLink,
|
||||
@ -146,7 +146,7 @@ export const iconList: { [name: string]: IconType } = {
|
||||
html: BsFiletypeHtml,
|
||||
info: FaInfo,
|
||||
ipynb: BsFiletypePy,
|
||||
key: FaKey,
|
||||
key: MdOutlineVpnKey,
|
||||
link: MdLink,
|
||||
linkedin: FaLinkedin,
|
||||
loader: AiOutlineLoading3Quarters,
|
||||
|
Loading…
Reference in New Issue
Block a user