diff --git a/apps/customer_dashboard/src/app/page.tsx b/apps/customer_dashboard/src/app/page.tsx index 3b1e190fd..57a07ed46 100644 --- a/apps/customer_dashboard/src/app/page.tsx +++ b/apps/customer_dashboard/src/app/page.tsx @@ -9,6 +9,9 @@ import { AccountInfo } from "@oko-wallet-ct-dashboard/components/account_info/ac import { HomeBanner } from "@oko-wallet-ct-dashboard/components/home_banner/home_banner"; // import { SDKInstallationGuide } from "@oko-wallet-ct-dashboard/components/sdk_installation_guide/sdk_installation_guide"; import { APIKeyList } from "@oko-wallet-ct-dashboard/components/api_key_list/api_key_list"; +import { DesignSection } from "@oko-wallet-ct-dashboard/components/design_section/design_section"; +import { Spacing } from "@oko-wallet-common-ui/spacing/spacing"; +import { ToastContainer } from "@oko-wallet-ct-dashboard/components/toast"; export default function Home() { return ( @@ -38,6 +41,9 @@ export default function Home() { {/* TODO: @Ryz0nd, @lidarbtc */} {/* */} + + + diff --git a/apps/customer_dashboard/src/components/api_key_list/api_key_list.tsx b/apps/customer_dashboard/src/components/api_key_list/api_key_list.tsx index 05471cf4a..90734f4fb 100644 --- a/apps/customer_dashboard/src/components/api_key_list/api_key_list.tsx +++ b/apps/customer_dashboard/src/components/api_key_list/api_key_list.tsx @@ -25,7 +25,7 @@ export const APIKeyList = () => {
diff --git a/apps/customer_dashboard/src/components/design_section/design_section.module.scss b/apps/customer_dashboard/src/components/design_section/design_section.module.scss new file mode 100644 index 000000000..a1e681253 --- /dev/null +++ b/apps/customer_dashboard/src/components/design_section/design_section.module.scss @@ -0,0 +1,56 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: 32px; +} + +.themeSection { + display: flex; + flex-direction: column; + width: 100%; + + gap: 16px; +} + +.themeOptions { + display: flex; + gap: 12px; + + flex-wrap: wrap; +} + +.themeActions { + display: flex; + gap: 12px; +} + +.themeOptionButton { + padding: 8px 12px; + border-radius: 10px; + border: 1px solid var(--border-primary); + background: var(--bg-primary); + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: #d0d5dd; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } +} + +.themeOptionButtonActive { + background: var(--bg-brand-solid); + color: var(--text-primary-on-brand); +} + +.error { + color: var(--text-error, #d92d20); + font-size: 14px; + font-weight: 500; + line-height: 20px; +} diff --git a/apps/customer_dashboard/src/components/design_section/design_section.tsx b/apps/customer_dashboard/src/components/design_section/design_section.tsx new file mode 100644 index 000000000..098ced1bf --- /dev/null +++ b/apps/customer_dashboard/src/components/design_section/design_section.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useLayoutEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { Button } from "@oko-wallet/oko-common-ui/button"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import type { CustomerTheme } from "@oko-wallet/oko-types/customers"; +import { Spacing } from "@oko-wallet/oko-common-ui/spacing"; + +import { useCustomerInfo } from "@oko-wallet-ct-dashboard/hooks/use_customer_info"; +import { useAppState } from "@oko-wallet-ct-dashboard/state"; +import { requestUpdateCustomerInfo } from "@oko-wallet-ct-dashboard/fetch/customers"; +import styles from "./design_section.module.scss"; +import { ThemeButton } from "./theme_button"; +import { displayToast } from "@oko-wallet-ct-dashboard/components/toast"; + +const THEME_OPTIONS: CustomerTheme[] = ["light", "dark", "system"]; + +export const DesignSection = () => { + const queryClient = useQueryClient(); + const customer = useCustomerInfo(); + const token = useAppState((state) => state.token); + const [savedTheme, setSavedTheme] = useState( + customer.data?.theme ?? "system", + ); + const [draftTheme, setDraftTheme] = useState( + customer.data?.theme ?? "system", + ); + + useLayoutEffect(() => { + if (customer.data?.theme) { + setSavedTheme(customer.data.theme); + setDraftTheme(customer.data.theme); + } + }, [customer.data?.theme]); + + const [isSavingTheme, setIsSavingTheme] = useState(false); + const [themeError, setThemeError] = useState(null); + + const hasThemeChange = draftTheme !== savedTheme; + + const handleThemeSelect = (option: CustomerTheme) => { + if (isSavingTheme) { + return; + } + + setThemeError(null); + setDraftTheme(option); + }; + + const handleThemeCancel = () => { + if (!hasThemeChange || isSavingTheme) { + return; + } + + setThemeError(null); + setDraftTheme(savedTheme); + }; + + const handleThemeSave = async () => { + if (!hasThemeChange || isSavingTheme) { + return; + } + + if (!token) { + setThemeError("Please log in to continue."); + return; + } + + setIsSavingTheme(true); + setThemeError(null); + + try { + const result = await requestUpdateCustomerInfo({ + token, + theme: draftTheme, + }); + + if (result.success) { + setSavedTheme(draftTheme); + displayToast({ + variant: "success", + title: "Saved", + }); + await queryClient.invalidateQueries({ queryKey: ["customer"] }); + } else { + setThemeError(result.msg ?? "Failed to update theme."); + } + } catch (err) { + setThemeError("An error occurred while updating the theme."); + } finally { + setIsSavingTheme(false); + } + }; + + return ( +
+
+ + Design + + + + + + You can configure the Oko UI theme integrated into your app. + +
+
+
+ {THEME_OPTIONS.map((option) => { + const label = + option === "system" + ? "System" + : option === "light" + ? "Light" + : "Dark"; + + return ( + handleThemeSelect(option)} + active={draftTheme === option} + label={label} + /> + ); + })} +
+ +
+ + +
+ + {themeError &&
{themeError}
} +
+
+ ); +}; diff --git a/apps/customer_dashboard/src/components/design_section/theme_button.module.scss b/apps/customer_dashboard/src/components/design_section/theme_button.module.scss new file mode 100644 index 000000000..8f609d08c --- /dev/null +++ b/apps/customer_dashboard/src/components/design_section/theme_button.module.scss @@ -0,0 +1,138 @@ +.activeIndicator { + position: absolute; + bottom: 8; + right: 7; + width: 16px; + height: 16px; + background-color: var(--border-brand-alt); + border-radius: 50%; +} + +.buttonContainer { + display: flex; + flex-direction: column; + + gap: 8px; +} +.button { + margin: 0; + padding: 0; + border: none; + background: none; + cursor: pointer; + outline: none; + box-shadow: none; + border-radius: 0; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; + + position: relative; + + width: 100px; + height: 60px; + border-radius: 8px; + border: 1px solid var(--border-primary); + overflow: hidden; + transition: border 0.1s ease-in-out; + + &:hover { + border: 2px solid var(--border-brand); + } + + &.active { + border: 2px solid var(--border-brand-alt); + } +} + +.lightThemeButton { + position: relative; + background-color: var(--gray-200); + overflow: hidden; + + .lightThemeButtonInner { + margin-top: 14px; + margin-left: 24px; + background-color: var(--white); + border: 1px solid var(--border-primary); + width: 80px; + height: 50px; + border-top-left-radius: 8px; + } + + .lightThemeButtonInnerText { + margin-top: 8px; + margin-left: 8px; + text-align: left; + } +} + +.darkThemeButton { + position: relative; + background-color: var(--gray-500); + overflow: hidden; + + .darkThemeButtonInner { + margin-top: 14px; + margin-left: 24px; + background-color: #22262f; + border: 1px solid #373a41; + width: 80px; + height: 50px; + border-top-left-radius: 8px; + } + + .darkThemeButtonInnerText { + margin-top: 8px; + margin-left: 8px; + text-align: left; + color: #f7f7f7; + } +} + +.systemThemeButtonContainer { + display: flex; + + .lightThemeButton { + position: relative; + background-color: var(--gray-200); + + .lightThemeButtonInner { + margin-top: 14px; + margin-left: 14px; + background-color: var(--white); + border: 1px solid var(--border-primary); + width: 80px; + height: 50px; + border-top-left-radius: 8px; + } + + .lightThemeButtonInnerText { + margin-top: 8px; + margin-left: 8px; + text-align: left; + } + } + + .darkThemeButton { + position: relative; + background-color: var(--gray-500); + + .darkThemeButtonInner { + margin-top: 14px; + margin-left: 14px; + background-color: #22262f; + border: 1px solid #373a41; + width: 80px; + height: 50px; + border-top-left-radius: 8px; + } + + .darkThemeButtonInnerText { + margin-top: 8px; + margin-left: 8px; + text-align: left; + color: #f7f7f7; + } + } +} diff --git a/apps/customer_dashboard/src/components/design_section/theme_button.tsx b/apps/customer_dashboard/src/components/design_section/theme_button.tsx new file mode 100644 index 000000000..03512912b --- /dev/null +++ b/apps/customer_dashboard/src/components/design_section/theme_button.tsx @@ -0,0 +1,112 @@ +import { CustomerTheme } from "@oko-wallet/oko-types/customers"; +import { Typography } from "@oko-wallet/oko-common-ui/typography"; +import { CheckIcon } from "@oko-wallet/oko-common-ui/icons/check_icon"; + +import styles from "./theme_button.module.scss"; + +const LightTheme = () => { + return ( +
+
+ + Aa + +
+
+ ); +}; + +const DarkTheme = () => { + return ( +
+
+ + Aa + +
+
+ ); +}; + +const SystemThemeButton = () => { + return ( +
+
+
+ + Aa + +
+
+ +
+
+ + Aa + +
+
+
+ ); +}; + +export const ThemeButton = ({ + theme, + onClick, + active, + label, +}: { + theme: CustomerTheme; + onClick: () => void; + active: boolean; + label: string; +}) => { + const themeButton = (() => { + switch (theme) { + case "light": + return ; + case "dark": + return ; + case "system": + return ; + } + })(); + + return ( +
+ + + + {label} + +
+ ); +}; diff --git a/apps/customer_dashboard/src/components/edit_info_form/edit_info_form.tsx b/apps/customer_dashboard/src/components/edit_info_form/edit_info_form.tsx index 0ddc300fb..28810606e 100644 --- a/apps/customer_dashboard/src/components/edit_info_form/edit_info_form.tsx +++ b/apps/customer_dashboard/src/components/edit_info_form/edit_info_form.tsx @@ -13,17 +13,13 @@ import { Input } from "@oko-wallet/oko-common-ui/input"; import { Button } from "@oko-wallet/oko-common-ui/button"; import { PlusIcon } from "@oko-wallet/oko-common-ui/icons/plus"; import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close"; -import type { CustomerTheme } from "@oko-wallet/oko-types/customers"; -import { Typography } from "@oko-wallet/oko-common-ui/typography"; import { useCustomerInfo } from "@oko-wallet-ct-dashboard/hooks/use_customer_info"; import { useAppState } from "@oko-wallet-ct-dashboard/state"; import { requestUpdateCustomerInfo } from "@oko-wallet-ct-dashboard/fetch/customers"; import styles from "./edit_info_form.module.scss"; -const THEME_OPTIONS: CustomerTheme[] = ["light", "dark", "system"]; - -export const EditInfoForm: FC = () => { +export const EditInfoForm = () => { const router = useRouter(); const queryClient = useQueryClient(); const customer = useCustomerInfo(); @@ -33,9 +29,6 @@ export const EditInfoForm: FC = () => { const [label, setLabel] = useState(customer.data?.label ?? ""); const [url, setUrl] = useState(customer.data?.url ?? ""); - const [theme, setTheme] = useState( - customer.data?.theme ?? "system", - ); const [logoFile, setLogoFile] = useState(null); const [previewUrl, setPreviewUrl] = useState( @@ -161,9 +154,8 @@ export const EditInfoForm: FC = () => { const hasLabelChange = label !== customer.data?.label; const hasUrlChange = url !== (customer.data?.url ?? ""); const hasLogoChange = logoFile !== null || shouldDeleteLogo; - const hasThemeChange = theme !== customer.data?.theme; - if (!hasLabelChange && !hasUrlChange && !hasLogoChange && !hasThemeChange) { + if (!hasLabelChange && !hasUrlChange && !hasLogoChange) { setError("No changes to save."); return; } @@ -187,7 +179,6 @@ export const EditInfoForm: FC = () => { label: hasLabelChange ? label : undefined, url: hasUrlChange ? url : undefined, logoFile: logoFile, - theme: hasThemeChange ? theme : undefined, deleteLogo: shouldDeleteLogo, }); @@ -215,8 +206,7 @@ export const EditInfoForm: FC = () => { label !== customer.data?.label || url !== (customer.data?.url ?? "") || logoFile !== null || - shouldDeleteLogo || - theme !== customer.data?.theme; + shouldDeleteLogo; return (
@@ -240,46 +230,6 @@ export const EditInfoForm: FC = () => { className={styles.input} /> -
-
- Oko Wallet Theme - - Choose the default theme for the Oko wallet. - -
- -
- {THEME_OPTIONS.map((option) => { - const label = - option === "system" - ? "System" - : option === "light" - ? "Light" - : "Dark"; - - return ( - - ); - })} -
-
- {/* Logo Upload with drag & drop */}
- + ); } diff --git a/apps/user_dashboard/src/components/toast/index.tsx b/apps/user_dashboard/src/components/toast/index.tsx index 99f58b097..b96b91c8a 100644 --- a/apps/user_dashboard/src/components/toast/index.tsx +++ b/apps/user_dashboard/src/components/toast/index.tsx @@ -1,5 +1,16 @@ -import { Toast, type ToastVariant } from "@oko-wallet/oko-common-ui/toast"; -import { type ToastOptions, toast } from "react-toastify"; +"use client"; + +import { + Toast, + ToastCloseButton, + type ToastVariant, +} from "@oko-wallet/oko-common-ui/toast"; +import { + type ToastOptions, + toast, + Bounce, + ToastContainer as ReactToastifyToastContainer, +} from "react-toastify"; export function displayToast({ variant, @@ -31,3 +42,16 @@ export function displayToast({ toastOptions, ); } + +export const ToastContainer = () => { + return ( + ( + + )} + /> + ); +}; diff --git a/ui/oko_common_ui/src/checkbox/checkbox.tsx b/ui/oko_common_ui/src/checkbox/checkbox.tsx index fc06460c8..00eb0c0db 100644 --- a/ui/oko_common_ui/src/checkbox/checkbox.tsx +++ b/ui/oko_common_ui/src/checkbox/checkbox.tsx @@ -2,11 +2,11 @@ import React from "react"; import cn from "classnames"; import styles from "./checkbox.module.scss"; -import { CheckIcon } from "./check_icon"; import { Typography, type BaseTypographyProps, } from "@oko-wallet-common-ui/typography/typography"; +import { CheckIcon } from "@oko-wallet-common-ui/icons/check_icon"; interface CheckboxProps { id: string; diff --git a/ui/oko_common_ui/src/checkbox/check_icon.tsx b/ui/oko_common_ui/src/icons/check_icon.tsx similarity index 100% rename from ui/oko_common_ui/src/checkbox/check_icon.tsx rename to ui/oko_common_ui/src/icons/check_icon.tsx