Skip to content

Commit 568c3b0

Browse files
customer_dashboard: add a prop "theme" to Customer entity
* [oko_pg_interface] add edit_customers for adding theme col * [backend] update customer's api of edit,create to store theme * [customer_dashboard] add theme button and some guardrail for blocking empty url * [common] add CustomerTheme type * [backed] add attached_api * [backend] add attached_api to server routes * [ui] add 'system' value in Theme type * [attached] get theme by using api * [embed] add useSetThemeInCallback and apply it * [backend] remove console * update yarn lock * [backed] reflect code review * [attached] remove setTheme - Since the callback is executed after the attached function is already running on the host, there is no need to call setTheme here. --------- Co-authored-by: Elden Park <eldeniyenden@gmail.com>
1 parent b113193 commit 568c3b0

File tree

106 files changed

+1218
-819
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+1218
-819
lines changed

apps/customer_dashboard/src/components/account_info/account_info.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { FC } from "react";
3+
import { type FC } from "react";
44
import { ExternalLinkOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/external_link_outlined";
55
import { Typography } from "@oko-wallet/oko-common-ui/typography";
66
import { Spacing } from "@oko-wallet/oko-common-ui/spacing";

apps/customer_dashboard/src/components/api_key_list/api_key_item_row.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { FC, useState } from "react";
3+
import { type FC, useState } from "react";
44
import { Badge } from "@oko-wallet/oko-common-ui/badge";
55
import { CopyOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/copy_outlined";
66
import { EyeIcon } from "@oko-wallet/oko-common-ui/icons/eye";

apps/customer_dashboard/src/components/authorized/authorized.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"use client";
22

3-
import { PropsWithChildren, useEffect, useState } from "react";
3+
import { type PropsWithChildren, useEffect, useState, type FC } from "react";
44
import { useRouter } from "next/navigation";
55

66
import { paths } from "@oko-wallet-ct-dashboard/paths";
77
import { useAppState } from "@oko-wallet-ct-dashboard/state";
88

9-
export const Authorized: React.FC<PropsWithChildren> = ({ children }) => {
9+
export const Authorized: FC<PropsWithChildren> = ({ children }) => {
1010
const router = useRouter();
1111
const user = useAppState((state) => state.user);
1212
const token = useAppState((state) => state.token);

apps/customer_dashboard/src/components/dashboard_body/dashboard_body.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropsWithChildren, FC } from "react";
1+
import type { PropsWithChildren, FC } from "react";
22
import cn from "classnames";
33

44
import styles from "./dashboard_body.module.scss";

apps/customer_dashboard/src/components/edit_info_form/edit_info_form.module.scss

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,61 @@
124124
width: 100%;
125125
margin-top: 8px;
126126
}
127+
128+
.themeSection {
129+
display: flex;
130+
flex-direction: column;
131+
gap: 12px;
132+
width: 100%;
133+
}
134+
135+
.themeHeader {
136+
display: flex;
137+
flex-direction: column;
138+
gap: 4px;
139+
}
140+
141+
.themeLabel {
142+
color: var(--text-secondary);
143+
font-size: 14px;
144+
font-weight: 600;
145+
line-height: 20px;
146+
}
147+
148+
.themeDescription {
149+
color: var(--text-secondary);
150+
font-size: 13px;
151+
font-weight: 500;
152+
line-height: 18px;
153+
}
154+
155+
.themeOptions {
156+
display: flex;
157+
gap: 8px;
158+
flex-wrap: wrap;
159+
}
160+
161+
.themeOptionButton {
162+
padding: 8px 12px;
163+
border-radius: 10px;
164+
border: 1px solid var(--border-primary);
165+
background: var(--bg-primary);
166+
color: var(--text-primary);
167+
168+
cursor: pointer;
169+
transition: all 0.2s ease;
170+
171+
&:hover {
172+
border-color: #d0d5dd;
173+
}
174+
175+
&:disabled {
176+
cursor: not-allowed;
177+
opacity: 0.6;
178+
}
179+
}
180+
181+
.themeOptionButtonActive {
182+
background: var(--bg-brand-solid);
183+
color: var(--text-primary-on-brand);
184+
}

apps/customer_dashboard/src/components/edit_info_form/edit_info_form.tsx

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
"use client";
22

3-
import React, { useRef, useState } from "react";
3+
import {
4+
type ChangeEvent,
5+
useRef,
6+
useState,
7+
type DragEvent,
8+
type FC,
9+
} from "react";
410
import { useRouter } from "next/navigation";
511
import { useQueryClient } from "@tanstack/react-query";
612
import { Input } from "@oko-wallet/oko-common-ui/input";
713
import { Button } from "@oko-wallet/oko-common-ui/button";
814
import { PlusIcon } from "@oko-wallet/oko-common-ui/icons/plus";
915
import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
16+
import type { CustomerTheme } from "@oko-wallet/oko-types/customers";
17+
import { Typography } from "@oko-wallet/oko-common-ui/typography";
1018

1119
import { useCustomerInfo } from "@oko-wallet-ct-dashboard/hooks/use_customer_info";
1220
import { useAppState } from "@oko-wallet-ct-dashboard/state";
1321
import { requestUpdateCustomerInfo } from "@oko-wallet-ct-dashboard/fetch/customers";
1422
import styles from "./edit_info_form.module.scss";
1523

16-
export const EditInfoForm = () => {
24+
const THEME_OPTIONS: CustomerTheme[] = ["light", "dark", "system"];
25+
26+
export const EditInfoForm: FC = () => {
1727
const router = useRouter();
1828
const queryClient = useQueryClient();
1929
const customer = useCustomerInfo();
@@ -23,6 +33,10 @@ export const EditInfoForm = () => {
2333

2434
const [label, setLabel] = useState(customer.data?.label ?? "");
2535
const [url, setUrl] = useState(customer.data?.url ?? "");
36+
const [theme, setTheme] = useState<CustomerTheme>(
37+
customer.data?.theme ?? "system",
38+
);
39+
2640
const [logoFile, setLogoFile] = useState<File | null>(null);
2741
const [previewUrl, setPreviewUrl] = useState<string | null>(
2842
customer.data?.logo_url ?? null,
@@ -95,7 +109,7 @@ export const EditInfoForm = () => {
95109
}
96110
};
97111

98-
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
112+
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
99113
const file = e.target.files?.[0];
100114
if (file) {
101115
handleLogoUpload(file);
@@ -107,29 +121,29 @@ export const EditInfoForm = () => {
107121
};
108122

109123
// Drag and drop handlers
110-
const handleDragEnter = (e: React.DragEvent) => {
124+
const handleDragEnter = (e: DragEvent) => {
111125
e.preventDefault();
112126
e.stopPropagation();
113127
setIsDragging(true);
114128
};
115129

116-
const handleDragLeave = (e: React.DragEvent) => {
130+
const handleDragLeave = (e: DragEvent) => {
117131
e.preventDefault();
118132
e.stopPropagation();
119133
setIsDragging(false);
120134
};
121135

122-
const handleDragOver = (e: React.DragEvent) => {
136+
const handleDragOver = (e: DragEvent) => {
123137
e.preventDefault();
124138
e.stopPropagation();
125139
};
126140

127-
const handleDrop = (e: React.DragEvent) => {
141+
const handleDrop = (e: DragEvent) => {
128142
e.preventDefault();
129143
e.stopPropagation();
130144
setIsDragging(false);
131145

132-
const file = e.dataTransfer.files?.[0];
146+
const file = e.dataTransfer?.files?.[0];
133147
if (file) {
134148
handleLogoUpload(file);
135149
}
@@ -147,12 +161,23 @@ export const EditInfoForm = () => {
147161
const hasLabelChange = label !== customer.data?.label;
148162
const hasUrlChange = url !== (customer.data?.url ?? "");
149163
const hasLogoChange = logoFile !== null || shouldDeleteLogo;
164+
const hasThemeChange = theme !== customer.data?.theme;
150165

151-
if (!hasLabelChange && !hasUrlChange && !hasLogoChange) {
166+
if (!hasLabelChange && !hasUrlChange && !hasLogoChange && !hasThemeChange) {
152167
setError("No changes to save.");
153168
return;
154169
}
155170

171+
if (!url || url.trim() === "") {
172+
setError("App URL is required.");
173+
return;
174+
}
175+
176+
if (hasUrlChange && !validateUrl(url)) {
177+
setError("App URL format is invalid.");
178+
return;
179+
}
180+
156181
setIsLoading(true);
157182
setError(null);
158183

@@ -162,6 +187,7 @@ export const EditInfoForm = () => {
162187
label: hasLabelChange ? label : undefined,
163188
url: hasUrlChange ? url : undefined,
164189
logoFile: logoFile,
190+
theme: hasThemeChange ? theme : undefined,
165191
deleteLogo: shouldDeleteLogo,
166192
});
167193

@@ -189,7 +215,8 @@ export const EditInfoForm = () => {
189215
label !== customer.data?.label ||
190216
url !== (customer.data?.url ?? "") ||
191217
logoFile !== null ||
192-
shouldDeleteLogo;
218+
shouldDeleteLogo ||
219+
theme !== customer.data?.theme;
193220

194221
return (
195222
<form onSubmit={handleSubmit} className={styles.form}>
@@ -213,6 +240,46 @@ export const EditInfoForm = () => {
213240
className={styles.input}
214241
/>
215242

243+
<div className={styles.themeSection}>
244+
<div className={styles.themeHeader}>
245+
<span className={styles.themeLabel}>Oko Wallet Theme</span>
246+
<span className={styles.themeDescription}>
247+
Choose the default theme for the Oko wallet.
248+
</span>
249+
</div>
250+
251+
<div className={styles.themeOptions}>
252+
{THEME_OPTIONS.map((option) => {
253+
const label =
254+
option === "system"
255+
? "System"
256+
: option === "light"
257+
? "Light"
258+
: "Dark";
259+
260+
return (
261+
<button
262+
key={option}
263+
type="button"
264+
className={`${styles.themeOptionButton} ${
265+
theme === option ? styles.themeOptionButtonActive : ""
266+
}`}
267+
onClick={() => setTheme(option)}
268+
disabled={isLoading}
269+
>
270+
<Typography
271+
size="sm"
272+
weight="medium"
273+
color={theme === option ? "primary-on-brand" : "primary"}
274+
>
275+
{label}
276+
</Typography>
277+
</button>
278+
);
279+
})}
280+
</div>
281+
</div>
282+
216283
{/* Logo Upload with drag & drop */}
217284
<div className={styles.appLogoUploadWrapper}>
218285
<label className={styles.appLogoUploadLabel}>
@@ -289,3 +356,12 @@ export const EditInfoForm = () => {
289356
</form>
290357
);
291358
};
359+
360+
function validateUrl(url: string): boolean {
361+
try {
362+
const urlObj = new URL(url);
363+
return urlObj.protocol === "https:" || urlObj.protocol === "http:";
364+
} catch {
365+
return false;
366+
}
367+
}

apps/customer_dashboard/src/components/expiry_timer/expiry_timer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
"use client";
22

3-
import { useEffect, useState, ReactNode, FC, useCallback } from "react";
3+
import {
4+
useEffect,
5+
useState,
6+
type ReactNode,
7+
type FC,
8+
useCallback,
9+
} from "react";
410

511
type ExpiryTimerProps = {
612
duration?: number;

apps/customer_dashboard/src/components/external_link_item/external_link_item.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC } from "react";
1+
import { type FC } from "react";
22
import Link from "next/link";
33
import { ExternalLinkOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/external_link_outlined";
44
import { Typography } from "@oko-wallet/oko-common-ui/typography";

apps/customer_dashboard/src/components/home_banner/home_banner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC } from "react";
1+
import { type FC } from "react";
22
import Link from "next/link";
33
import { ArrowRightOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/arrow_right_outlined";
44
import { Typography } from "@oko-wallet/oko-common-ui/typography";

apps/customer_dashboard/src/components/providers/providers.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
QueryClient,
77
QueryClientProvider,
88
} from "@tanstack/react-query";
9-
import React, { PropsWithChildren } from "react";
9+
import { type FC, type PropsWithChildren } from "react";
1010

1111
function makeTanStackQueryClient() {
1212
// Create a client
@@ -16,7 +16,7 @@ function makeTanStackQueryClient() {
1616

1717
const queryClient = makeTanStackQueryClient();
1818

19-
export const Providers: React.FC<PropsWithChildren> = ({ children }) => {
19+
export const Providers: FC<PropsWithChildren> = ({ children }) => {
2020
return (
2121
<QueryClientProvider client={queryClient}>
2222
<HydrationBoundary state={dehydrate(queryClient)}>

0 commit comments

Comments
 (0)