feat(topics) - add list topic
This commit is contained in:
@@ -25,35 +25,41 @@ export default function NavigationLayout({
|
||||
action,
|
||||
mainClassName,
|
||||
bgImage,
|
||||
backText = 'back'
|
||||
backText = "back",
|
||||
}: NavigationLayoutProps) {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-linear-to-b from-gray-1 to-gray-2 h-full flex flex-col relative overflow-y-auto",
|
||||
className,
|
||||
bgImage && 'bg-cover bg-top-left'
|
||||
bgImage && "bg-cover bg-top-left",
|
||||
)}
|
||||
style={{
|
||||
...(bgImage ? { backgroundImage: `url(${bgImage})` } : {})
|
||||
...(bgImage ? { backgroundImage: `url(${bgImage})` } : {}),
|
||||
}}
|
||||
>
|
||||
{ bgImage && <div className="absolute left-0 top-0 h-full w-full bg-dark-1 opacity-20"/> }
|
||||
{bgImage && (
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-dark-1 opacity-20" />
|
||||
)}
|
||||
{title && (
|
||||
<div className="text-base font-bold text-neutral-900 w-full text-center p-4">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('flex items-center justify-between relative w-full', title && 'absolute w-full left-0 top-0')}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between relative w-full",
|
||||
title && "absolute w-full left-0 top-0",
|
||||
)}
|
||||
>
|
||||
{isBack && (
|
||||
<span
|
||||
onClick={() => router.history.back()}
|
||||
className={cn(
|
||||
"flex justify-start shrink-0 items-center gap-2 text-base font-semibold cursor-pointer w-fit p-4",
|
||||
backClassName
|
||||
backClassName,
|
||||
)}
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
@@ -66,7 +72,7 @@ export default function NavigationLayout({
|
||||
className={cn(
|
||||
"h-full relative pt-18",
|
||||
isBottomNav && "overflow-y-auto",
|
||||
mainClassName
|
||||
mainClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Link, useRouterState } from "@tanstack/react-router";
|
||||
import { BarChart3, DoorOpen, Home, Settings } from "lucide-react";
|
||||
import { BarChart3, Home, ListCheck, Settings } from "lucide-react";
|
||||
import { cn } from "../../../utils/classname";
|
||||
|
||||
|
||||
export default function BottomNavigation() {
|
||||
const location = useRouterState({
|
||||
select: (state) => state.location,
|
||||
})
|
||||
|
||||
console.log(location.pathname)
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ path: "/", icon: Home, label: "Dashboard", disabled: false },
|
||||
{ path: "/analysis", icon: BarChart3, label: "analysis", disabled: true },
|
||||
{ path: "/rooms", icon: DoorOpen, label: "Rooms", disabled: true },
|
||||
{ path: "/topics", icon: ListCheck, label: "Topics", disabled: false },
|
||||
{ path: "/settings", icon: Settings, label: "Settings", disabled: false },
|
||||
];
|
||||
|
||||
@@ -24,7 +21,11 @@ console.log(location.pathname)
|
||||
<Link
|
||||
key={key}
|
||||
to={tab.path}
|
||||
className={cn('flex-1 py-4 flex flex-col items-center gap-1 transition-colors text-neutral-900 disabled:text-neutral-300', tab.path === location.pathname && 'text-orange-500', tab.disabled && 'pointer-events-none text-neutral-300')}
|
||||
className={cn(
|
||||
"flex-1 py-4 flex flex-col items-center gap-1 transition-colors text-neutral-900 disabled:text-neutral-300",
|
||||
tab.path === location.pathname && "text-orange-500",
|
||||
tab.disabled && "pointer-events-none text-neutral-300",
|
||||
)}
|
||||
>
|
||||
<tab.icon size={24} />
|
||||
<span className="text-xs font-medium">{tab.label}</span>
|
||||
|
||||
@@ -2,4 +2,6 @@ export const ENV = {
|
||||
apiUrl: import.meta.env.VITE_API_URL,
|
||||
apiKey: import.meta.env.VITE_SIGN_KEY,
|
||||
secretKey: import.meta.env.VITE_SIGN_SECRET_KEY,
|
||||
}
|
||||
basicUsername: import.meta.env.VITE_BASIC_AUTH_USERNAME,
|
||||
basicPassword: import.meta.env.VITE_BASIC_AUTH_PASSWORD,
|
||||
};
|
||||
|
||||
@@ -11,15 +11,13 @@ export default function SettingsFeature() {
|
||||
tower: "",
|
||||
floor: "",
|
||||
unit: "",
|
||||
credential: "",
|
||||
});
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
function isObjectValueDifferent(
|
||||
a: MerchantForm,
|
||||
b: MerchantForm
|
||||
): boolean {
|
||||
function isObjectValueDifferent(a: MerchantForm, b: MerchantForm): boolean {
|
||||
return (Object.keys(a) as (keyof MerchantForm)[]).some(
|
||||
(key) => a[key] !== b[key]
|
||||
(key) => a[key] !== b[key],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,11 +39,8 @@ function handleSubmit(e: React.FormEvent) {
|
||||
if (isDifferent) {
|
||||
clearDevice(); // reset device kalau merchant berubah
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
setLoading(true);
|
||||
saveMerchant(form);
|
||||
|
||||
@@ -119,6 +114,18 @@ function handleSubmit(e: React.FormEvent) {
|
||||
className="p-2 border border-neutral-200 w-full rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 text-neutral-900 block font-semibold">
|
||||
Credential
|
||||
</label>
|
||||
<input
|
||||
name="credential"
|
||||
value={form.credential}
|
||||
onChange={handleChange}
|
||||
className="p-2 border border-neutral-200 w-full rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
14
src/features/topics/hooks/queries.ts
Normal file
14
src/features/topics/hooks/queries.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getTopicsState } from "../../../repositories/device";
|
||||
|
||||
export function useTopics() {
|
||||
const result = useQuery({
|
||||
queryKey: ["topics/state"],
|
||||
queryFn: () => getTopicsState(),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result?.data?.data,
|
||||
};
|
||||
}
|
||||
28
src/features/topics/index.tsx
Normal file
28
src/features/topics/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Edit, PlusIcon, Trash2 } from "lucide-react";
|
||||
import { useTopics } from "./hooks/queries";
|
||||
|
||||
export default function TopicsFeature() {
|
||||
const { data } = useTopics();
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="p-4 flex justify-end">
|
||||
<button className="shrink-0 px-4 py-2 rounded-md flex items-center justify-center transition-all bg-orange-500 text-white cursor-pointer">
|
||||
<PlusIcon />
|
||||
Tambah Topic
|
||||
</button>
|
||||
</div>
|
||||
{data?.map((item, key) => (
|
||||
<div
|
||||
key={key}
|
||||
className="bg-white border-b border-neutral-200 p-4 flex justify-between"
|
||||
>
|
||||
<h1>{item.topic}</h1>
|
||||
<div className="flex gap-2 ">
|
||||
<Edit className="text-blue-500 cursor-pointer" size={18} />
|
||||
<Trash2 className="text-red-500 cursor-pointer" size={18} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/pages/topics/index.tsx
Normal file
15
src/pages/topics/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import NavigationLayout from "../../components/layout/navigation.layout";
|
||||
import TopicsFeature from "../../features/topics";
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<NavigationLayout
|
||||
title="Topics"
|
||||
mainClassName="pt-2"
|
||||
className="from-white to-white"
|
||||
isBack
|
||||
>
|
||||
<TopicsFeature />
|
||||
</NavigationLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,27 @@
|
||||
import { ENV } from "../../constants/env";
|
||||
import api from "../../utils/axios";
|
||||
import { basicAuth } from "../../utils/basic-auth";
|
||||
|
||||
export const getDeviceStatus = async (params: DeviceParams): Promise<Device> => {
|
||||
const res = await api.get('/device/v1/status', { params });
|
||||
export const getDeviceStatus = async (
|
||||
params: DeviceParams,
|
||||
): Promise<Device> => {
|
||||
const res = await api.get("/device/v1/status", { params });
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postCommandStatus = async (payload: DevicePayload): Promise<Device> => {
|
||||
const res = await api.post('/device/v1/command', payload);
|
||||
export const postCommandStatus = async (
|
||||
payload: DevicePayload,
|
||||
): Promise<Device> => {
|
||||
const res = await api.post("/device/v1/command", payload);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const getTopicsState = async (): Promise<TopicData> => {
|
||||
const res = await api.get("/topics/v1/commands", {
|
||||
headers: {
|
||||
Authorization: basicAuth(ENV.basicUsername, ENV.basicPassword),
|
||||
},
|
||||
});
|
||||
|
||||
return res.data;
|
||||
};
|
||||
40
src/repositories/device/types.d.ts
vendored
40
src/repositories/device/types.d.ts
vendored
@@ -5,7 +5,7 @@ type Device = {
|
||||
};
|
||||
|
||||
type DeviceData = {
|
||||
id?: string,
|
||||
id?: string;
|
||||
floorName?: string;
|
||||
unitNumber?: string;
|
||||
deviceName?: string;
|
||||
@@ -14,11 +14,11 @@ type DeviceData = {
|
||||
code?: string;
|
||||
towerNumber?: string;
|
||||
payload?: string;
|
||||
deviceLabel?: string
|
||||
deviceName?: string
|
||||
active?: boolean
|
||||
status?: boolean
|
||||
refetch?: () => void
|
||||
deviceLabel?: string;
|
||||
deviceName?: string;
|
||||
active?: boolean;
|
||||
status?: boolean;
|
||||
refetch?: () => void;
|
||||
};
|
||||
|
||||
type DeviceParams = {
|
||||
@@ -33,15 +33,21 @@ type DeviceParams = {
|
||||
};
|
||||
|
||||
type DevicePayload = {
|
||||
commandType?: string,
|
||||
deviceName?: string,
|
||||
deviceType?: string,
|
||||
floorName?: string,
|
||||
merchantName?: string,
|
||||
commandType?: string;
|
||||
deviceName?: string;
|
||||
deviceType?: string;
|
||||
floorName?: string;
|
||||
merchantName?: string;
|
||||
payload?: {
|
||||
action?: string
|
||||
},
|
||||
roomName?: string,
|
||||
towerNumber?: string,
|
||||
unitNumber?: string,
|
||||
}
|
||||
action?: string;
|
||||
};
|
||||
roomName?: string;
|
||||
towerNumber?: string;
|
||||
unitNumber?: string;
|
||||
};
|
||||
|
||||
type TopicData = {
|
||||
status: string;
|
||||
message: string;
|
||||
data: { topic: string }[];
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export const AppNavigationRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
id: "app",
|
||||
component: lazyRouteComponent(
|
||||
() => import("../components/layout/main-navigation.layout")
|
||||
() => import("../components/layout/main-navigation.layout"),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -20,5 +20,3 @@ export const AnalysisRoute = createRoute({
|
||||
path: "/analysis",
|
||||
component: lazyRouteComponent(() => import("../pages/analysis")),
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export const AppRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
id: "general",
|
||||
component: lazyRouteComponent(
|
||||
() => import("../components/layout/main.layout")
|
||||
() => import("../components/layout/main.layout"),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -32,3 +32,9 @@ export const SettingsRoute = createRoute({
|
||||
path: "/settings",
|
||||
component: lazyRouteComponent(() => import("../pages/settings")),
|
||||
});
|
||||
|
||||
export const TopicsRoute = createRoute({
|
||||
getParentRoute: () => AppRoute,
|
||||
path: "/topics",
|
||||
component: lazyRouteComponent(() => import("../pages/topics")),
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DetailDeviceRoute,
|
||||
RoomsRoute,
|
||||
SettingsRoute,
|
||||
TopicsRoute,
|
||||
} from "./app.route";
|
||||
|
||||
const routeTree = RootRoute.addChildren([
|
||||
@@ -22,6 +23,7 @@ const routeTree = RootRoute.addChildren([
|
||||
RoomsRoute,
|
||||
DetailDeviceRoute,
|
||||
SettingsRoute,
|
||||
TopicsRoute,
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
1
src/types/merchant.d.ts
vendored
1
src/types/merchant.d.ts
vendored
@@ -3,4 +3,5 @@ type MerchantForm = {
|
||||
tower: string;
|
||||
floor: string;
|
||||
unit: string;
|
||||
credential: string;
|
||||
};
|
||||
|
||||
@@ -6,8 +6,7 @@ import { ENV } from "../constants/env";
|
||||
|
||||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
interface SignedAxiosRequestConfig
|
||||
extends InternalAxiosRequestConfig {
|
||||
interface SignedAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
@@ -16,7 +15,6 @@ const api = axios.create({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
|
||||
api.interceptors.request.use(
|
||||
(config: SignedAxiosRequestConfig) => {
|
||||
if (config.signed === false) {
|
||||
@@ -50,10 +48,9 @@ api.interceptors.request.use(
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
|
||||
// RESPONSE INTERCEPTOR (Optional)
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
@@ -62,7 +59,7 @@ api.interceptors.response.use(
|
||||
console.warn("Unauthorized");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
3
src/utils/basic-auth.ts
Normal file
3
src/utils/basic-auth.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const basicAuth = (username: string, password: string) => {
|
||||
return "Basic " + btoa(`${username}:${password}`);
|
||||
};
|
||||
Reference in New Issue
Block a user