feat(topics) - add list topic
This commit is contained in:
@@ -25,35 +25,41 @@ export default function NavigationLayout({
|
|||||||
action,
|
action,
|
||||||
mainClassName,
|
mainClassName,
|
||||||
bgImage,
|
bgImage,
|
||||||
backText = 'back'
|
backText = "back",
|
||||||
}: NavigationLayoutProps) {
|
}: NavigationLayoutProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-linear-to-b from-gray-1 to-gray-2 h-full flex flex-col relative overflow-y-auto",
|
"bg-linear-to-b from-gray-1 to-gray-2 h-full flex flex-col relative overflow-y-auto",
|
||||||
className,
|
className,
|
||||||
bgImage && 'bg-cover bg-top-left'
|
bgImage && "bg-cover bg-top-left",
|
||||||
)}
|
)}
|
||||||
style={{
|
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 && (
|
{title && (
|
||||||
<div className="text-base font-bold text-neutral-900 w-full text-center p-4">
|
<div className="text-base font-bold text-neutral-900 w-full text-center p-4">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</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 && (
|
{isBack && (
|
||||||
<span
|
<span
|
||||||
onClick={() => router.history.back()}
|
onClick={() => router.history.back()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex justify-start shrink-0 items-center gap-2 text-base font-semibold cursor-pointer w-fit p-4",
|
"flex justify-start shrink-0 items-center gap-2 text-base font-semibold cursor-pointer w-fit p-4",
|
||||||
backClassName
|
backClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
@@ -66,7 +72,7 @@ export default function NavigationLayout({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"h-full relative pt-18",
|
"h-full relative pt-18",
|
||||||
isBottomNav && "overflow-y-auto",
|
isBottomNav && "overflow-y-auto",
|
||||||
mainClassName
|
mainClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { Link, useRouterState } from "@tanstack/react-router";
|
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";
|
import { cn } from "../../../utils/classname";
|
||||||
|
|
||||||
|
|
||||||
export default function BottomNavigation() {
|
export default function BottomNavigation() {
|
||||||
const location = useRouterState({
|
const location = useRouterState({
|
||||||
select: (state) => state.location,
|
select: (state) => state.location,
|
||||||
})
|
});
|
||||||
|
|
||||||
console.log(location.pathname)
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ path: "/", icon: Home, label: "Dashboard", disabled: false },
|
{ path: "/", icon: Home, label: "Dashboard", disabled: false },
|
||||||
{ path: "/analysis", icon: BarChart3, label: "analysis", disabled: true },
|
{ 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 },
|
{ path: "/settings", icon: Settings, label: "Settings", disabled: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -24,7 +21,11 @@ console.log(location.pathname)
|
|||||||
<Link
|
<Link
|
||||||
key={key}
|
key={key}
|
||||||
to={tab.path}
|
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} />
|
<tab.icon size={24} />
|
||||||
<span className="text-xs font-medium">{tab.label}</span>
|
<span className="text-xs font-medium">{tab.label}</span>
|
||||||
|
|||||||
@@ -2,4 +2,6 @@ export const ENV = {
|
|||||||
apiUrl: import.meta.env.VITE_API_URL,
|
apiUrl: import.meta.env.VITE_API_URL,
|
||||||
apiKey: import.meta.env.VITE_SIGN_KEY,
|
apiKey: import.meta.env.VITE_SIGN_KEY,
|
||||||
secretKey: import.meta.env.VITE_SIGN_SECRET_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,17 +11,15 @@ export default function SettingsFeature() {
|
|||||||
tower: "",
|
tower: "",
|
||||||
floor: "",
|
floor: "",
|
||||||
unit: "",
|
unit: "",
|
||||||
|
credential: "",
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
function isObjectValueDifferent(
|
function isObjectValueDifferent(a: MerchantForm, b: MerchantForm): boolean {
|
||||||
a: MerchantForm,
|
return (Object.keys(a) as (keyof MerchantForm)[]).some(
|
||||||
b: MerchantForm
|
(key) => a[key] !== b[key],
|
||||||
): boolean {
|
);
|
||||||
return (Object.keys(a) as (keyof MerchantForm)[]).some(
|
}
|
||||||
(key) => a[key] !== b[key]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
@@ -31,30 +29,27 @@ function isObjectValueDifferent(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent) {
|
function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const merchant = getMerchant();
|
const merchant = getMerchant();
|
||||||
|
|
||||||
if(merchant){
|
if (merchant) {
|
||||||
const isDifferent = isObjectValueDifferent(merchant, form);
|
const isDifferent = isObjectValueDifferent(merchant, form);
|
||||||
if (isDifferent) {
|
if (isDifferent) {
|
||||||
clearDevice(); // reset device kalau merchant berubah
|
clearDevice(); // reset device kalau merchant berubah
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
saveMerchant(form);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
navigate({ to: "/" });
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
saveMerchant(form);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoading(false);
|
|
||||||
navigate({ to: "/" });
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const merchant = getMerchant();
|
const merchant = getMerchant();
|
||||||
if (merchant) {
|
if (merchant) {
|
||||||
@@ -119,6 +114,18 @@ function handleSubmit(e: React.FormEvent) {
|
|||||||
className="p-2 border border-neutral-200 w-full rounded-md"
|
className="p-2 border border-neutral-200 w-full rounded-md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</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 api from "../../utils/axios";
|
||||||
|
import { basicAuth } from "../../utils/basic-auth";
|
||||||
|
|
||||||
export const getDeviceStatus = async (params: DeviceParams): Promise<Device> => {
|
export const getDeviceStatus = async (
|
||||||
const res = await api.get('/device/v1/status', { params });
|
params: DeviceParams,
|
||||||
|
): Promise<Device> => {
|
||||||
|
const res = await api.get("/device/v1/status", { params });
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postCommandStatus = async (payload: DevicePayload): Promise<Device> => {
|
export const postCommandStatus = async (
|
||||||
const res = await api.post('/device/v1/command', payload);
|
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;
|
return res.data;
|
||||||
};
|
};
|
||||||
60
src/repositories/device/types.d.ts
vendored
60
src/repositories/device/types.d.ts
vendored
@@ -5,21 +5,21 @@ type Device = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DeviceData = {
|
type DeviceData = {
|
||||||
id?: string,
|
id?: string;
|
||||||
floorName?: string;
|
floorName?: string;
|
||||||
unitNumber?: string;
|
unitNumber?: string;
|
||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
deviceType?: string;
|
deviceType?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
towerNumber?: string;
|
towerNumber?: string;
|
||||||
payload?: string;
|
payload?: string;
|
||||||
deviceLabel?: string
|
deviceLabel?: string;
|
||||||
deviceName?: string
|
deviceName?: string;
|
||||||
active?: boolean
|
active?: boolean;
|
||||||
status?: boolean
|
status?: boolean;
|
||||||
refetch?: () => void
|
refetch?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeviceParams = {
|
type DeviceParams = {
|
||||||
merchantName?: string;
|
merchantName?: string;
|
||||||
@@ -33,15 +33,21 @@ type DeviceParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DevicePayload = {
|
type DevicePayload = {
|
||||||
commandType?: string,
|
commandType?: string;
|
||||||
deviceName?: string,
|
deviceName?: string;
|
||||||
deviceType?: string,
|
deviceType?: string;
|
||||||
floorName?: string,
|
floorName?: string;
|
||||||
merchantName?: string,
|
merchantName?: string;
|
||||||
payload?: {
|
payload?: {
|
||||||
action?: string
|
action?: string;
|
||||||
},
|
};
|
||||||
roomName?: string,
|
roomName?: string;
|
||||||
towerNumber?: string,
|
towerNumber?: string;
|
||||||
unitNumber?: string,
|
unitNumber?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type TopicData = {
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
data: { topic: string }[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const AppNavigationRoute = createRoute({
|
|||||||
getParentRoute: () => RootRoute,
|
getParentRoute: () => RootRoute,
|
||||||
id: "app",
|
id: "app",
|
||||||
component: lazyRouteComponent(
|
component: lazyRouteComponent(
|
||||||
() => import("../components/layout/main-navigation.layout")
|
() => import("../components/layout/main-navigation.layout"),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,5 +20,3 @@ export const AnalysisRoute = createRoute({
|
|||||||
path: "/analysis",
|
path: "/analysis",
|
||||||
component: lazyRouteComponent(() => import("../pages/analysis")),
|
component: lazyRouteComponent(() => import("../pages/analysis")),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const AppRoute = createRoute({
|
|||||||
getParentRoute: () => RootRoute,
|
getParentRoute: () => RootRoute,
|
||||||
id: "general",
|
id: "general",
|
||||||
component: lazyRouteComponent(
|
component: lazyRouteComponent(
|
||||||
() => import("../components/layout/main.layout")
|
() => import("../components/layout/main.layout"),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,3 +32,9 @@ export const SettingsRoute = createRoute({
|
|||||||
path: "/settings",
|
path: "/settings",
|
||||||
component: lazyRouteComponent(() => import("../pages/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,
|
DetailDeviceRoute,
|
||||||
RoomsRoute,
|
RoomsRoute,
|
||||||
SettingsRoute,
|
SettingsRoute,
|
||||||
|
TopicsRoute,
|
||||||
} from "./app.route";
|
} from "./app.route";
|
||||||
|
|
||||||
const routeTree = RootRoute.addChildren([
|
const routeTree = RootRoute.addChildren([
|
||||||
@@ -22,6 +23,7 @@ const routeTree = RootRoute.addChildren([
|
|||||||
RoomsRoute,
|
RoomsRoute,
|
||||||
DetailDeviceRoute,
|
DetailDeviceRoute,
|
||||||
SettingsRoute,
|
SettingsRoute,
|
||||||
|
TopicsRoute,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
1
src/types/merchant.d.ts
vendored
1
src/types/merchant.d.ts
vendored
@@ -3,4 +3,5 @@ type MerchantForm = {
|
|||||||
tower: string;
|
tower: string;
|
||||||
floor: string;
|
floor: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
credential: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { ENV } from "../constants/env";
|
|||||||
|
|
||||||
import type { InternalAxiosRequestConfig } from "axios";
|
import type { InternalAxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
interface SignedAxiosRequestConfig
|
interface SignedAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||||
extends InternalAxiosRequestConfig {
|
|
||||||
signed?: boolean;
|
signed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ const api = axios.create({
|
|||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config: SignedAxiosRequestConfig) => {
|
(config: SignedAxiosRequestConfig) => {
|
||||||
if (config.signed === false) {
|
if (config.signed === false) {
|
||||||
@@ -50,10 +48,9 @@ api.interceptors.request.use(
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => Promise.reject(error),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// RESPONSE INTERCEPTOR (Optional)
|
// RESPONSE INTERCEPTOR (Optional)
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
@@ -62,7 +59,7 @@ api.interceptors.response.use(
|
|||||||
console.warn("Unauthorized");
|
console.warn("Unauthorized");
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default api;
|
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