From b7bc8f552503fa121497bd237fe8f663c582e768 Mon Sep 17 00:00:00 2001 From: kasun Date: Wed, 24 Jun 2026 00:58:45 +0200 Subject: [PATCH] added first searchmodal draft --- .../app/search/HomeboxSearchProvider.java | 8 +++ frontend/src/api/searches.ts | 9 +++ frontend/src/components/ActionButton.scss | 35 ++++++++++ frontend/src/components/ActionButton.tsx | 17 +++++ frontend/src/components/Dashboard.tsx | 32 ++++++--- frontend/src/components/SearchModal.tsx | 69 +++++++++++++++++++ frontend/src/components/ServiceCard.scss | 35 ---------- frontend/src/components/ServiceCard.tsx | 17 ++--- frontend/src/types/search.ts | 6 ++ 9 files changed, 175 insertions(+), 53 deletions(-) create mode 100644 frontend/src/api/searches.ts create mode 100644 frontend/src/components/ActionButton.scss create mode 100644 frontend/src/components/ActionButton.tsx create mode 100644 frontend/src/components/SearchModal.tsx create mode 100644 frontend/src/types/search.ts diff --git a/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java b/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java index e2b7f57..d21f9c6 100644 --- a/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java +++ b/backend/src/main/java/com/vaessl/app/search/HomeboxSearchProvider.java @@ -15,11 +15,17 @@ import com.vaessl.app.connection.HomeboxEntity; import com.vaessl.app.exception.ConnectionNotFoundException; import com.vaessl.app.exception.RemoteApiException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import static com.vaessl.app.connection.Endpoint.*; @Component public class HomeboxSearchProvider implements SearchProvider { + // TODO: Remove logger before merge + private static final Logger log = LoggerFactory.getLogger(HomeboxSearchProvider.class); + private final RestClient.Builder restClientBuilder; private final ConnectionRepository cRepository; @@ -64,6 +70,8 @@ public class HomeboxSearchProvider implements SearchProvider { return new SearchResponse(title, description, extraSearchResponseData); }).toList(); + log.info("Search results for query '{}': {}", request.query(), items); + return new PageImpl<>(items, pageable, hbResponse.total()); } diff --git a/frontend/src/api/searches.ts b/frontend/src/api/searches.ts new file mode 100644 index 0000000..ad471f7 --- /dev/null +++ b/frontend/src/api/searches.ts @@ -0,0 +1,9 @@ +import { apiFetch } from "./client"; +import type { AuthResponse } from "../types/connection"; +import type { SearchRequest } from "../types/search"; + +export const search = (req: SearchRequest) => + apiFetch("/search", { + method: "POST", + body: JSON.stringify(req), + }); diff --git a/frontend/src/components/ActionButton.scss b/frontend/src/components/ActionButton.scss new file mode 100644 index 0000000..6a71384 --- /dev/null +++ b/frontend/src/components/ActionButton.scss @@ -0,0 +1,35 @@ +.action-btn { + font-size: 14px; + font-weight: 500; + padding: 7px 16px; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + transition: box-shadow 0.2s, opacity 0.2s; + + &:hover { + opacity: 0.85; + } + + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + + &--connect, + &--search { + background: var(--accent); + color: #fff; + } + + &--disconnect { + background: transparent; + color: var(--text); + border-color: var(--border); + + &:hover { + border-color: #ef4444; + color: #ef4444; + } + } +} diff --git a/frontend/src/components/ActionButton.tsx b/frontend/src/components/ActionButton.tsx new file mode 100644 index 0000000..37c54b8 --- /dev/null +++ b/frontend/src/components/ActionButton.tsx @@ -0,0 +1,17 @@ +import './ActionButton.scss' + +type Variant = 'connect' | 'disconnect' | 'search' + +interface Props { + variant: Variant + onClick: () => void + children: React.ReactNode +} + +export function ActionButton({ variant, onClick, children }: Readonly) { + return ( + + ) +} diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index b75a625..35c1f76 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -4,6 +4,7 @@ import type { ConnectionStatus } from "../types/connection" import { ServiceCard } from "./ServiceCard" import { ConnectModal } from "./ConnectModal" import "./Dashboard.scss" +import { SearchModal } from "./SearchModal" const SERVICES = [ { serviceType: 'HOMEBOX', label: 'Homebox', icon: '📦' }, @@ -11,7 +12,8 @@ const SERVICES = [ export function Dashboard() { const [statuses, setStatuses] = useState([]) - const [openModal, setOpenModal] = useState(null) + const [openConnectionModal, setOpenConnectionModal] = useState(null) + const [openSearchModal, setOpenSearchModal] = useState(null) const refresh = () => { getStatuses().then(setStatuses).catch(() => { }) @@ -23,7 +25,9 @@ export function Dashboard() { logout(serviceType).then(refresh).catch(() => { }) } - const activeModal = SERVICES.find(s => s.serviceType === openModal) + const activeConnectionModal = SERVICES.find(s => s.serviceType === openConnectionModal) + const activeSearchModal = SERVICES.find(s => s.serviceType === openSearchModal) + const activeSearchStatus = statuses.find(s => s.serviceType === openSearchModal) return (
@@ -40,18 +44,30 @@ export function Dashboard() { label={label} icon={icon} status={statuses.find(s => s.serviceType === serviceType) ?? null} - onConnect={() => setOpenModal(serviceType)} + onConnect={() => setOpenConnectionModal(serviceType)} onDisconnect={() => handleDisconnect(serviceType)} + onSearch={() => setOpenSearchModal(serviceType)} /> ))}
- {activeModal && ( + {activeConnectionModal && ( setOpenModal(null)} - onSuccess={() => { setOpenModal(null); refresh() }} + serviceType={activeConnectionModal.serviceType} + label={activeConnectionModal.label} + onClose={() => setOpenSearchModal(null)} + onSuccess={() => { setOpenSearchModal(null); refresh() }} + /> + )} + + {activeSearchModal && ( + setOpenSearchModal(null) } + /> )} diff --git a/frontend/src/components/SearchModal.tsx b/frontend/src/components/SearchModal.tsx new file mode 100644 index 0000000..1677d00 --- /dev/null +++ b/frontend/src/components/SearchModal.tsx @@ -0,0 +1,69 @@ +import { useEffect, useRef, useState, type SyntheticEvent } from 'react' +import './ConnectModal.scss' +import type { SearchRequest } from '../types/search' +import { search } from '../api/searches' + +interface Props { + serviceType: string + label: string + appUrl: string + username: string + onClose: () => void +} + +export function SearchModal({ serviceType, label, appUrl, username, onClose }: Readonly) { + const [query, setQuery] = useState(''); + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const firstInputRef = useRef(null) + + const dialogRef = useRef(null) + + useEffect(() => { + const dialog = dialogRef.current + if (!dialog) return + + dialog.showModal() + firstInputRef.current?.focus() + + const handleCancel = (e: Event) => { + e.preventDefault() + onClose() + } + dialog.addEventListener('cancel', handleCancel) + return () => dialog.removeEventListener('cancel', handleCancel) + }, [onClose]) + + const handleSubmit = async (e: SyntheticEvent) => { + e.preventDefault() + setError(null) + setLoading(true) + try { + const req: SearchRequest = { appUrl, serviceType, username, query} + await search(req) + } catch (err) { + setError(err instanceof Error ? err.message : 'Search failed') + } finally { + setLoading(false) + } + } + + return ( + +
+ + +
+
+
+ setQuery(e.target.value)} /> +
+ {error &&

{error}

} + +
+
+ ) +} diff --git a/frontend/src/components/ServiceCard.scss b/frontend/src/components/ServiceCard.scss index b35c55c..bab31b0 100644 --- a/frontend/src/components/ServiceCard.scss +++ b/frontend/src/components/ServiceCard.scss @@ -81,39 +81,4 @@ display: flex; gap: 8px; } - - &__btn { - font-size: 14px; - font-weight: 500; - padding: 7px 16px; - border-radius: 6px; - border: 1px solid transparent; - cursor: pointer; - transition: box-shadow 0.2s, opacity 0.2s; - - &:hover { - opacity: 0.85; - } - - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } - - &--connect { - background: var(--accent); - color: #fff; - } - - &--disconnect { - background: transparent; - color: var(--text); - border-color: var(--border); - - &:hover { - border-color: #ef4444; - color: #ef4444; - } - } - } } \ No newline at end of file diff --git a/frontend/src/components/ServiceCard.tsx b/frontend/src/components/ServiceCard.tsx index 3b1bc5d..5a5bb3c 100644 --- a/frontend/src/components/ServiceCard.tsx +++ b/frontend/src/components/ServiceCard.tsx @@ -1,4 +1,5 @@ import type { ConnectionStatus } from '../types/connection' +import { ActionButton } from './ActionButton' import './ServiceCard.scss' interface Props { @@ -8,9 +9,10 @@ interface Props { status: ConnectionStatus | null onConnect: () => void onDisconnect: () => void + onSearch: () => void } -export function ServiceCard({ serviceType: _serviceType, label, icon, status, onConnect, onDisconnect }: Readonly) { +export function ServiceCard({ serviceType: _serviceType, label, icon, status, onConnect, onDisconnect, onSearch }: Readonly) { const connected = status?.connected ?? false const formatExpiry = (iso: string | null) => { @@ -37,15 +39,10 @@ export function ServiceCard({ serviceType: _serviceType, label, icon, status, on
- {connected ? ( - - ) : ( - - )} + + {connected ? 'Disconnect' : 'Connect'} + + {connected ? (Search) : null}
) diff --git a/frontend/src/types/search.ts b/frontend/src/types/search.ts new file mode 100644 index 0000000..c195a92 --- /dev/null +++ b/frontend/src/types/search.ts @@ -0,0 +1,6 @@ +export interface SearchRequest { + appUrl: string; + serviceType: string; + username: string; + query: string; +}