diff --git a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/MessageRow.tsx b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/MessageRow.tsx index 511eb41e5ec0..81c7d9ac5937 100644 --- a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/MessageRow.tsx +++ b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/MessageRow.tsx @@ -1,10 +1,10 @@ import React from "react"; +import { CopyButton } from "@/lib/components/ui/CopyButton"; import Icon from "@/lib/components/ui/Icon/Icon"; import { Source } from "@/lib/types/MessageMetadata"; import styles from "./MessageRow.module.scss"; -import { CopyButton } from "./components/CopyButton"; import { MessageContent } from "./components/MessageContent/MessageContent"; import { QuestionBrain } from "./components/QuestionBrain/QuestionBrain"; import { QuestionPrompt } from "./components/QuestionPrompt/QuestionPrompt"; @@ -26,7 +26,7 @@ export const MessageRow = React.forwardRef( { speaker, text, brainName, promptName, children }: MessageRowProps, ref: React.Ref ) => { - const { handleCopy, isCopied, isUserSpeaker } = useMessageRow({ + const { handleCopy, isUserSpeaker } = useMessageRow({ speaker, text, }); @@ -58,7 +58,7 @@ export const MessageRow = React.forwardRef( {!isUserSpeaker && messageContent !== "🧠" && (
- +
)} diff --git a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/components/CopyButton.tsx b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/components/CopyButton.tsx deleted file mode 100644 index 3b62d024fb1c..000000000000 --- a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/components/CopyButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Icon from "@/lib/components/ui/Icon/Icon"; - -type CopyButtonProps = { - handleCopy: () => void; - isCopied: boolean; -}; - -export const CopyButton = ({ - handleCopy, - isCopied, -}: CopyButtonProps): JSX.Element => ( - -); diff --git a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/hooks/useMessageRow.ts b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/hooks/useMessageRow.ts index 1c6e34c02bfb..9e3980f37b68 100644 --- a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/hooks/useMessageRow.ts +++ b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/hooks/useMessageRow.ts @@ -1,5 +1,3 @@ -import { useState } from "react"; - type UseMessageRowProps = { speaker: "user" | "assistant"; text?: string; @@ -8,22 +6,16 @@ type UseMessageRowProps = { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const useMessageRow = ({ speaker, text }: UseMessageRowProps) => { const isUserSpeaker = speaker === "user"; - const [isCopied, setIsCopied] = useState(false); const handleCopy = () => { if (text === undefined) { return; } - navigator.clipboard.writeText(text).then( - () => setIsCopied(true), - (err) => console.error("Failed to copy!", err) - ); - setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds + navigator.clipboard.writeText(text).catch((err) => console.error(err)); }; return { isUserSpeaker, - isCopied, handleCopy, }; }; diff --git a/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.module.scss b/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.module.scss new file mode 100644 index 000000000000..e09c7b7592e7 --- /dev/null +++ b/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.module.scss @@ -0,0 +1,6 @@ +@use "@/styles/Spacings.module.scss"; + +.response_wrapper { + display: flex; + gap: Spacings.$spacing03; +} diff --git a/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx b/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx index 6eedaf4e97c4..280745089175 100644 --- a/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx +++ b/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx @@ -2,45 +2,35 @@ "use client"; import { useTranslation } from "react-i18next"; -import { FaCopy } from "react-icons/fa"; import Button from "@/lib/components/ui/Button"; -import Field from "@/lib/components/ui/Field"; +import { CopyButton } from "@/lib/components/ui/CopyButton"; +import { FieldHeader } from "@/lib/components/ui/FieldHeader/FieldHeader"; +import styles from "./ApiKeyConfig.module.scss"; import { useApiKeyConfig } from "./hooks/useApiKeyConfig"; export const ApiKeyConfig = (): JSX.Element => { - const { - apiKey, - handleCopyClick, - handleCreateClick, - - } = useApiKeyConfig(); + const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig(); const { t } = useTranslation(["config"]); return ( - <> -

Quivr {t("apiKey")}

- -
- {apiKey === "" ? ( - - ) : ( -
- - -
- )} -
- - +
+ + {apiKey === "" ? ( + + ) : ( +
+ {apiKey} + +
+ )} +
); }; diff --git a/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts b/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts index 406c4392c73c..22e2c8914d73 100644 --- a/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts +++ b/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts @@ -63,10 +63,7 @@ export const useApiKeyConfig = () => { try { setChangeOpenAiApiKeyRequestPending(true); - - - await updateUserIdentity({ - }); + await updateUserIdentity({}); void queryClient.invalidateQueries({ queryKey: [USER_IDENTITY_DATA_KEY], }); @@ -85,8 +82,7 @@ export const useApiKeyConfig = () => { const removeOpenAiApiKey = async () => { try { setChangeOpenAiApiKeyRequestPending(true); - await updateUserIdentity({ - }); + await updateUserIdentity({}); publish({ variant: "success", @@ -103,8 +99,6 @@ export const useApiKeyConfig = () => { } }; - - return { handleCreateClick, apiKey, diff --git a/frontend/app/user/components/BrainsUsage/BrainsUsage.module.scss b/frontend/app/user/components/BrainsUsage/BrainsUsage.module.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frontend/app/user/components/BrainsUsage/BrainsUsage.tsx b/frontend/app/user/components/BrainsUsage/BrainsUsage.tsx new file mode 100644 index 000000000000..183423ec960d --- /dev/null +++ b/frontend/app/user/components/BrainsUsage/BrainsUsage.tsx @@ -0,0 +1,5 @@ +import { UserStatistics } from "../UserStatistics"; + +export const BrainsUsage = (): JSX.Element => { + return ; +}; diff --git a/frontend/app/user/components/LanguageSelect/LanguageSelect.tsx b/frontend/app/user/components/LanguageSelect/LanguageSelect.tsx index d8ebdfa3bf14..df8a155a5a44 100644 --- a/frontend/app/user/components/LanguageSelect/LanguageSelect.tsx +++ b/frontend/app/user/components/LanguageSelect/LanguageSelect.tsx @@ -2,36 +2,25 @@ 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 { allLanguages, currentLanguage, change } = useLanguageHook(); + const { currentLanguage, change } = useLanguageHook(); + + if (!currentLanguage) { + return <>; + } return ( -
- - - -
+ ); }; diff --git a/frontend/app/user/components/LanguageSelect/hooks/useLanguageHook.ts b/frontend/app/user/components/LanguageSelect/hooks/useLanguageHook.ts index e47608e20b5f..2f8fa8d387db 100644 --- a/frontend/app/user/components/LanguageSelect/hooks/useLanguageHook.ts +++ b/frontend/app/user/components/LanguageSelect/hooks/useLanguageHook.ts @@ -3,58 +3,70 @@ import { useTranslation } from "react-i18next"; import { useEventTracking } from "@/services/analytics/june/useEventTracking"; -export const languages = { - en: { +export type Language = { + label: string; + flag: string; + shortName: string; +}; + +export const languages: Language[] = [ + { label: "English", + flag: "🇬🇧", + shortName: "en", }, - es: { + { label: "Español", + flag: "🇪🇸", + shortName: "es", }, - fr: { + { label: "Français", + flag: "🇫🇷", + shortName: "fr", }, - ptbr: { + { label: "Português", + flag: "🇵🇹", + shortName: "pt", }, - ru: { + { label: "Русский", + flag: "🇷🇺", + shortName: "ru", }, - zh_cn: { + { label: "简体中文", + flag: "🇨🇳", + shortName: "zh", }, -}; - -export type Language = { - [key: string]: { - label: string; - }; -}; +]; export const useLanguageHook = (): { - change: (newLanguage: string) => void; - allLanguages: Language; - currentLanguage: string | undefined; + change: (newLanguage: Language) => void; + allLanguages: Language[]; + currentLanguage: Language | undefined; } => { const { i18n } = useTranslation(); - const [allLanguages, setAllLanguages] = useState({}); - const [currentLanguage, setCurrentLanguage] = useState(); + const [allLanguages, setAllLanguages] = useState([]); + const [currentLanguage, setCurrentLanguage] = useState(); const { track } = useEventTracking(); useEffect(() => { setAllLanguages(languages); + const savedLanguage = localStorage.getItem("selectedLanguage") ?? "English"; - // get language from localStorage - const savedLanguage = localStorage.getItem("selectedLanguage") ?? "en"; - - setCurrentLanguage(savedLanguage); + setCurrentLanguage( + languages.find((language) => language.label === savedLanguage) + ); void i18n.changeLanguage(savedLanguage); }, [i18n]); - const change = (newLanguage: string) => { + const change = (newLanguage: Language) => { void track("CHANGE_LANGUAGE"); setCurrentLanguage(newLanguage); - localStorage.setItem("selectedLanguage", newLanguage); - void i18n.changeLanguage(newLanguage); + localStorage.setItem("selectedLanguage", newLanguage.label); + void i18n.changeLanguage(newLanguage.shortName); }; return { diff --git a/frontend/app/user/components/LogoutCard/LogoutModal.tsx b/frontend/app/user/components/LogoutModal/LogoutModal.tsx similarity index 83% rename from frontend/app/user/components/LogoutCard/LogoutModal.tsx rename to frontend/app/user/components/LogoutModal/LogoutModal.tsx index 3760591c313c..98fc32b722d3 100644 --- a/frontend/app/user/components/LogoutCard/LogoutModal.tsx +++ b/frontend/app/user/components/LogoutModal/LogoutModal.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import Button from "@/lib/components/ui/Button"; import { Modal } from "@/lib/components/ui/Modal"; +import TextButton from "@/lib/components/ui/TextButton/TextButton"; import { useLogoutModal } from "./hooks/useLogoutModal"; @@ -17,9 +18,13 @@ export const LogoutModal = (): JSX.Element => { return ( - {t("logoutButton")} - +
void 0}> + +
} isOpen={isLogoutModalOpened} setOpen={setIsLogoutModalOpened} diff --git a/frontend/app/user/components/LogoutCard/hooks/__tests__/useLogoutModal.ts b/frontend/app/user/components/LogoutModal/hooks/__tests__/useLogoutModal.ts similarity index 100% rename from frontend/app/user/components/LogoutCard/hooks/__tests__/useLogoutModal.ts rename to frontend/app/user/components/LogoutModal/hooks/__tests__/useLogoutModal.ts diff --git a/frontend/app/user/components/LogoutCard/hooks/useLogoutModal.ts b/frontend/app/user/components/LogoutModal/hooks/useLogoutModal.ts similarity index 100% rename from frontend/app/user/components/LogoutCard/hooks/useLogoutModal.ts rename to frontend/app/user/components/LogoutModal/hooks/useLogoutModal.ts diff --git a/frontend/app/user/components/Plan/Plan.module.scss b/frontend/app/user/components/Plan/Plan.module.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frontend/app/user/components/Plan/Plan.tsx b/frontend/app/user/components/Plan/Plan.tsx new file mode 100644 index 000000000000..299e8dc94dac --- /dev/null +++ b/frontend/app/user/components/Plan/Plan.tsx @@ -0,0 +1,9 @@ +import { StripePricingOrManageButton } from "../StripePricingOrManageButton"; + +export const Plan = (): JSX.Element => { + return ( +
+ +
+ ); +}; diff --git a/frontend/app/user/components/Settings/Settings.module.scss b/frontend/app/user/components/Settings/Settings.module.scss new file mode 100644 index 000000000000..84e23ac00d27 --- /dev/null +++ b/frontend/app/user/components/Settings/Settings.module.scss @@ -0,0 +1,12 @@ +@use "@/styles/Radius.module.scss"; +@use "@/styles/Spacings.module.scss"; + +.settings_wrapper { + display: flex; + flex-direction: column; + gap: Spacings.$spacing07; + width: auto; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25); + border-radius: Radius.$big; + padding: Spacings.$spacing05; +} diff --git a/frontend/app/user/components/Settings/Settings.tsx b/frontend/app/user/components/Settings/Settings.tsx new file mode 100644 index 000000000000..743f2091df75 --- /dev/null +++ b/frontend/app/user/components/Settings/Settings.tsx @@ -0,0 +1,24 @@ +import { InfoDisplayer } from "@/lib/components/ui/InfoDisplayer/InfoDisplayer"; + +import styles from "./Settings.module.scss"; + +import { ApiKeyConfig } from "../ApiKeyConfig"; +import LanguageSelect from "../LanguageSelect/LanguageSelect"; +import { LogoutModal } from "../LogoutModal/LogoutModal"; + +type InfoDisplayerProps = { + email: string; +}; + +export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => { + return ( +
+ + {email} + + + + +
+ ); +}; diff --git a/frontend/app/user/components/UserMenuCard/UserMenuCard.module.scss b/frontend/app/user/components/UserMenuCard/UserMenuCard.module.scss new file mode 100644 index 000000000000..23f8fa7b490a --- /dev/null +++ b/frontend/app/user/components/UserMenuCard/UserMenuCard.module.scss @@ -0,0 +1,45 @@ +@use "@/styles/Colors.module.scss"; +@use "@/styles/Radius.module.scss"; +@use "@/styles/Spacings.module.scss"; +@use "@/styles/ScreenSizes.module.scss"; +@use "@/styles/Typography.module.scss"; + +.menu_card_container { + padding: Spacings.$spacing05; + border-radius: Radius.$big; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25); + cursor: pointer; + display: flex; + flex-direction: column; + gap: Spacings.$spacing03; + width: 20%; + + @media (max-width: ScreenSizes.$small) { + width: auto; + + .title, + .subtitle { + display: none; + } + } + + &:hover, + &.selected { + background-color: Colors.$primary-lightest; + } + + .first_line_wrapper { + display: flex; + align-items: center; + justify-content: space-between; + + .title { + @include Typography.H2; + } + } + + .subtitle { + font-size: Typography.$small; + color: Colors.$normal-grey; + } +} diff --git a/frontend/app/user/components/UserMenuCard/UserMenuCard.tsx b/frontend/app/user/components/UserMenuCard/UserMenuCard.tsx new file mode 100644 index 000000000000..a8aa231c366b --- /dev/null +++ b/frontend/app/user/components/UserMenuCard/UserMenuCard.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; + +import { Icon } from "@/lib/components/ui/Icon/Icon"; + +import styles from "./UserMenuCard.module.scss"; + +import { UserMenuCardProps } from "../types/types"; + +export const UserMenuCard = ({ + title, + subtitle, + iconName, + selected, + onClick, +}: UserMenuCardProps): JSX.Element => { + const [isHovered, setIsHovered] = useState(false); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + > +
+ {title} + +
+ {subtitle} +
+ ); +}; diff --git a/frontend/app/user/components/types/types.ts b/frontend/app/user/components/types/types.ts new file mode 100644 index 000000000000..8706b3f650e6 --- /dev/null +++ b/frontend/app/user/components/types/types.ts @@ -0,0 +1,7 @@ +export type UserMenuCardProps = { + title: string; + subtitle: string; + iconName: string; + selected: boolean; + onClick?: () => void; +}; diff --git a/frontend/app/user/page.module.scss b/frontend/app/user/page.module.scss new file mode 100644 index 000000000000..47793febbe68 --- /dev/null +++ b/frontend/app/user/page.module.scss @@ -0,0 +1,13 @@ +@use "@/styles/Spacings.module.scss"; + +.user_page_container { + padding: Spacings.$spacing09; + display: flex; + flex-direction: column; + gap: Spacings.$spacing09; + + .left_menu_wrapper { + display: flex; + gap: Spacings.$spacing05; + } +} diff --git a/frontend/app/user/page.tsx b/frontend/app/user/page.tsx index ae59da822dbf..88b18e7295c6 100644 --- a/frontend/app/user/page.tsx +++ b/frontend/app/user/page.tsx @@ -1,86 +1,76 @@ "use client"; -import Link from "next/link"; -import { useTranslation } from "react-i18next"; -import Button from "@/lib/components/ui/Button"; -import Card, { CardBody, CardHeader } from "@/lib/components/ui/Card"; +import { useState } from "react"; + import { useSupabase } from "@/lib/context/SupabaseProvider"; +import { useUserData } from "@/lib/hooks/useUserData"; import { redirectToLogin } from "@/lib/router/redirectToLogin"; -import { StripePricingOrManageButton, UserStatistics } from "./components"; -import { ApiKeyConfig } from "./components/ApiKeyConfig"; -import LanguageSelect from "./components/LanguageSelect/LanguageSelect"; -import { LogoutModal } from "./components/LogoutCard/LogoutModal"; +import { BrainsUsage } from "./components/BrainsUsage/BrainsUsage"; +import { Plan } from "./components/Plan/Plan"; +import { Settings } from "./components/Settings/Settings"; +import { UserMenuCard } from "./components/UserMenuCard/UserMenuCard"; +import { UserMenuCardProps } from "./components/types/types"; +import styles from "./page.module.scss"; const UserPage = (): JSX.Element => { const { session } = useSupabase(); + const { userData } = useUserData(); + + const [userMenuCards, setUserMenuCards] = useState([ + { + title: "Settings", + subtitle: "Change your settings", + iconName: "settings", + selected: true, + }, + { + title: "Brain Usage", + subtitle: "View your brain usage", + iconName: "graph", + selected: false, + }, + { + title: "Plan", + subtitle: "Manage your plan", + iconName: "unlock", + selected: false, + }, + ]); + + const handleCardClick = (index: number) => { + setUserMenuCards( + userMenuCards.map((card, i) => ({ + ...card, + selected: i === index, + })) + ); + }; - if (!session) { + if (!session || !userData) { redirectToLogin(); } - const { user } = session; - const { t } = useTranslation(["translation", "user", "config", "chat"]); - return ( <> -
- - - - - -

- {t("accountSection", { ns: "config" })} -

-
- - -
-

- {t("email")}: {user.email} -

- - -
- -
-
- - -

- {t("settings", { ns: "config" })} -

-
- - - - -
- - -

- {t("brainUsage", { ns: "user" })} -

-
- - - - -
- - -

- {t("apiKey", { ns: "config" })} -

-
- - - - -
+
+
+ {userMenuCards.map((card, index) => ( + handleCardClick(index)} + /> + ))} +
+
+ {userMenuCards[0].selected && } + {userMenuCards[1].selected && } + {userMenuCards[2].selected && } +
); diff --git a/frontend/lib/components/Menu/Menu.tsx b/frontend/lib/components/Menu/Menu.tsx index 06505071cec3..f373e35ca495 100644 --- a/frontend/lib/components/Menu/Menu.tsx +++ b/frontend/lib/components/Menu/Menu.tsx @@ -37,6 +37,7 @@ export const Menu = (): JSX.Element => { "/library", "/brains-management", "/search", + "/user", ]; const isMenuDisplayed = displayedOnPages.some((page) => diff --git a/frontend/lib/components/Menu/components/DiscussionButton/DiscussionButton.tsx b/frontend/lib/components/Menu/components/DiscussionButton/DiscussionButton.tsx index adbf34220485..554f6b1d4211 100644 --- a/frontend/lib/components/Menu/components/DiscussionButton/DiscussionButton.tsx +++ b/frontend/lib/components/Menu/components/DiscussionButton/DiscussionButton.tsx @@ -10,8 +10,8 @@ export const DiscussionButton = (): JSX.Element => { const { setIsVisible } = useSearchModalContext(); const handleClick = (event: React.MouseEvent) => { - event.stopPropagation(); setIsVisible(true); + event.nativeEvent.stopImmediatePropagation(); }; return ( diff --git a/frontend/lib/components/ui/CopyButton.tsx b/frontend/lib/components/ui/CopyButton.tsx new file mode 100644 index 000000000000..aa78f0ca014c --- /dev/null +++ b/frontend/lib/components/ui/CopyButton.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; + +import Icon from "@/lib/components/ui/Icon/Icon"; + +type CopyButtonProps = { + handleCopy: () => void; +}; + +export const CopyButton = ({ handleCopy }: CopyButtonProps): JSX.Element => { + const [isCopied, setIsCopied] = useState(false); + + const handleClick = () => { + handleCopy(); + setIsCopied(true); + }; + + useEffect(() => { + if (isCopied) { + const timer = setTimeout(() => { + setIsCopied(false); + }, 2000); + + return () => { + clearTimeout(timer); + }; + } + }, [isCopied]); + + return ( + + ); +}; diff --git a/frontend/lib/components/ui/CountrySelector/CountrySelector.module.scss b/frontend/lib/components/ui/CountrySelector/CountrySelector.module.scss new file mode 100644 index 000000000000..d89d6a657afb --- /dev/null +++ b/frontend/lib/components/ui/CountrySelector/CountrySelector.module.scss @@ -0,0 +1,17 @@ +@use "@/styles/Colors.module.scss"; +@use "@/styles/Radius.module.scss"; + +.selection { + border-radius: Radius.$normal; + box-shadow: none; + cursor: pointer; + + &:hover { + background-color: Colors.$lightest-black; + } + + &:focus { + box-shadow: none; + border-color: Colors.$primary; + } +} diff --git a/frontend/lib/components/ui/CountrySelector/CountrySelector.tsx b/frontend/lib/components/ui/CountrySelector/CountrySelector.tsx new file mode 100644 index 000000000000..e55232ae34da --- /dev/null +++ b/frontend/lib/components/ui/CountrySelector/CountrySelector.tsx @@ -0,0 +1,54 @@ +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 ( +
+ + +
+ ); +}; diff --git a/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss b/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss new file mode 100644 index 000000000000..6dffeeb25264 --- /dev/null +++ b/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss @@ -0,0 +1,9 @@ +@use "@/styles/Spacings.module.scss"; + +.field_header_wrapper { + display: flex; + gap: Spacings.$spacing03; + font-weight: bold; + align-items: center; + padding-bottom: Spacings.$spacing02; +} diff --git a/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx b/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx new file mode 100644 index 000000000000..ab4bd96e72d2 --- /dev/null +++ b/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx @@ -0,0 +1,20 @@ +import styles from "./FieldHeader.module.scss"; + +import { Icon } from "../Icon/Icon"; + +type FieldHeaderProps = { + iconName: string; + label: string; +}; + +export const FieldHeader = ({ + iconName, + label, +}: FieldHeaderProps): JSX.Element => { + return ( +
+ + +
+ ); +}; diff --git a/frontend/lib/components/ui/Icon/Icon.module.scss b/frontend/lib/components/ui/Icon/Icon.module.scss index 0d2778abc93c..197ddcb42430 100644 --- a/frontend/lib/components/ui/Icon/Icon.module.scss +++ b/frontend/lib/components/ui/Icon/Icon.module.scss @@ -53,6 +53,14 @@ } } +.dangerous { + color: Colors.$dangerous; + + &.hovered { + color: Colors.$dangerous-dark; + } +} + .disabled { color: Colors.$black; pointer-events: none; diff --git a/frontend/lib/components/ui/Icon/Icon.tsx b/frontend/lib/components/ui/Icon/Icon.tsx index c4d7050ac89b..c1af7c22b1e7 100644 --- a/frontend/lib/components/ui/Icon/Icon.tsx +++ b/frontend/lib/components/ui/Icon/Icon.tsx @@ -40,11 +40,11 @@ export const Icon = ({ return ( handleHover && setIconHovered(true)} onMouseLeave={() => handleHover && setIconHovered(false)} diff --git a/frontend/lib/components/ui/InfoDisplayer/InfoDisplayer.module.scss b/frontend/lib/components/ui/InfoDisplayer/InfoDisplayer.module.scss new file mode 100644 index 000000000000..e2bfadd1ff2f --- /dev/null +++ b/frontend/lib/components/ui/InfoDisplayer/InfoDisplayer.module.scss @@ -0,0 +1,6 @@ +@use "@/styles/Spacings.module.scss"; + +.info_displayer_container { + display: flex; + flex-direction: column; +} diff --git a/frontend/lib/components/ui/InfoDisplayer/InfoDisplayer.tsx b/frontend/lib/components/ui/InfoDisplayer/InfoDisplayer.tsx new file mode 100644 index 000000000000..5b3a73c5831b --- /dev/null +++ b/frontend/lib/components/ui/InfoDisplayer/InfoDisplayer.tsx @@ -0,0 +1,22 @@ +import styles from "./InfoDisplayer.module.scss"; + +import { FieldHeader } from "../FieldHeader/FieldHeader"; + +type InfoDisplayerProps = { + iconName: string; + label: string; + children: React.ReactNode; +}; + +export const InfoDisplayer = ({ + iconName, + label, + children, +}: InfoDisplayerProps): JSX.Element => { + return ( +
+ + {children} +
+ ); +}; diff --git a/frontend/lib/components/ui/TextButton/TextButton.module.scss b/frontend/lib/components/ui/TextButton/TextButton.module.scss index c7535d0f38cf..cc9deedb4232 100644 --- a/frontend/lib/components/ui/TextButton/TextButton.module.scss +++ b/frontend/lib/components/ui/TextButton/TextButton.module.scss @@ -16,3 +16,11 @@ color: Colors.$primary; } } + +.dangerous { + color: Colors.$dangerous; + + &.hovered { + color: Colors.$dangerous-dark; + } +} diff --git a/frontend/lib/components/ui/TextInput/TextInput.module.scss b/frontend/lib/components/ui/TextInput/TextInput.module.scss new file mode 100644 index 000000000000..1b533b57d2bb --- /dev/null +++ b/frontend/lib/components/ui/TextInput/TextInput.module.scss @@ -0,0 +1,20 @@ +@use "@/styles/Colors.module.scss"; +@use "@/styles/Radius.module.scss"; +@use "@/styles/Spacings.module.scss"; + +.text_input_container { + display: flex; + flex-direction: column; + gap: Spacings.$spacing03; + + .text_input { + border: 1px solid Colors.$lighter-grey; + border-radius: Radius.$big; + padding: Spacings.$spacing03; + caret-color: Colors.$accent; + + &:focus { + box-shadow: none; + } + } +} diff --git a/frontend/lib/components/ui/TextInput/TextInput.tsx b/frontend/lib/components/ui/TextInput/TextInput.tsx new file mode 100644 index 000000000000..22d3861e968d --- /dev/null +++ b/frontend/lib/components/ui/TextInput/TextInput.tsx @@ -0,0 +1,29 @@ +import styles from "./TextInput.module.scss"; + +import { FieldHeader } from "../FieldHeader/FieldHeader"; + +type TextInputProps = { + iconName: string; + label: string; + inputValue: string; + setInputValue: (value: string) => void; +}; + +export const TextInput = ({ + iconName, + label, + inputValue, + setInputValue, +}: TextInputProps): JSX.Element => { + return ( +
+ + setInputValue(e.target.value)} + /> +
+ ); +}; diff --git a/frontend/lib/helpers/iconList.ts b/frontend/lib/helpers/iconList.ts index 5e9e70c45d37..895c6386ad09 100644 --- a/frontend/lib/helpers/iconList.ts +++ b/frontend/lib/helpers/iconList.ts @@ -1,14 +1,17 @@ import { AiOutlineLoading3Quarters } from "react-icons/ai"; import { BsArrowRightShort } from "react-icons/bs"; +import { CiFlag1 } from "react-icons/ci"; import { FaCheck, FaCheckCircle, + FaKey, FaRegStar, FaRegUserCircle, + FaUnlock, } from "react-icons/fa"; import { FaArrowUpFromBracket } from "react-icons/fa6"; -import { IoIosAdd, IoMdClose } from "react-icons/io"; -import { IoHomeOutline } from "react-icons/io5"; +import { IoIosAdd, IoMdClose, IoMdLogOut } from "react-icons/io"; +import { IoHomeOutline, IoSettingsSharp } from "react-icons/io5"; import { IconType } from "react-icons/lib"; import { LuBrain, @@ -20,8 +23,15 @@ import { LuPlusCircle, LuSearch, } from "react-icons/lu"; -import { MdDelete, MdEdit, MdHistory, MdUploadFile } from "react-icons/md"; +import { + MdAlternateEmail, + MdDelete, + MdEdit, + MdHistory, + MdUploadFile, +} from "react-icons/md"; import { RiHashtag } from "react-icons/ri"; +import { VscGraph } from "react-icons/vsc"; export const iconList: { [name: string]: IconType } = { add: LuPlusCircle, @@ -36,15 +46,22 @@ export const iconList: { [name: string]: IconType } = { copy: LuCopy, delete: MdDelete, edit: MdEdit, + email: MdAlternateEmail, file: LuFile, + flag: CiFlag1, followUp: FaArrowUpFromBracket, + graph: VscGraph, hastag: RiHashtag, history: MdHistory, home: IoHomeOutline, + key: FaKey, loader: AiOutlineLoading3Quarters, + logout: IoMdLogOut, redirection: BsArrowRightShort, search: LuSearch, + settings: IoSettingsSharp, star: FaRegStar, + unlock: FaUnlock, upload: MdUploadFile, user: FaRegUserCircle, }; diff --git a/frontend/lib/types/Colors.ts b/frontend/lib/types/Colors.ts index 383913ccf4d4..c8313abf2994 100644 --- a/frontend/lib/types/Colors.ts +++ b/frontend/lib/types/Colors.ts @@ -4,4 +4,5 @@ export type Color = | "primary" | "gold" | "accent" - | "white"; + | "white" + | "dangerous"; diff --git a/frontend/styles/_Colors.module.scss b/frontend/styles/_Colors.module.scss index cf2aa3fe9ea9..b84e38d560c8 100644 --- a/frontend/styles/_Colors.module.scss +++ b/frontend/styles/_Colors.module.scss @@ -26,3 +26,7 @@ $dark-grey: #707070; // GOLD $gold: #b8860b; + +// ERROR +$dangerous-dark: #e30c17; +$dangerous: #9b373c;