added search response into searchmodal ui, restructured folders

This commit is contained in:
2026-06-24 15:57:10 +02:00
parent a1ce085123
commit 6e3d3347e2
16 changed files with 110 additions and 34 deletions
@@ -64,10 +64,11 @@ public class HomeboxSearchProvider implements SearchProvider {
}
List<SearchResponse> items = hbResponse.items().stream().map(i -> {
String id = i.id();
String title = i.name();
String description = i.description();
Map<String, Object> extraSearchResponseData = Map.of("location", i.parent());
return new SearchResponse(title, description, extraSearchResponseData);
return new SearchResponse(id, title, description, extraSearchResponseData);
}).toList();
log.info("Search results for query '{}': {}", request.query(), items);
@@ -79,7 +80,7 @@ public class HomeboxSearchProvider implements SearchProvider {
List<HomeboxItem> items) {
}
private record HomeboxItem(String name, String description, HomeboxLocation parent) {
private record HomeboxItem(String id, String name, String description, HomeboxLocation parent) {
}
private record HomeboxLocation(String name, String description) {
@@ -2,7 +2,8 @@ package com.vaessl.app.search;
import java.util.Map;
public record SearchResponse(String title, String description, Map<String, Object> extraData) {
public record SearchResponse(String id, String title, String description,
Map<String, Object> extraData) {
public String getExtra(String key) {
if (extraData == null) {
@@ -8,6 +8,7 @@ public final class Mockdata {
public static final String MOCK_SERVICE_TYPE = "SERVICE_TYPE";
public static final String MOCK_USER = "user";
public static final String MOCK_PASS = "pw";
public static final String MOCK_ID = "item-1";
public static final String MOCK_TITLE = "title";
public static final String MOCK_DESCRIPTION = "desc";
}
@@ -9,7 +9,7 @@ class SearchResponseTest {
@Test
void shouldReturnNullWhenExtraDataIsNull() {
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, null);
SearchResponse response = new SearchResponse(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, null);
assertThat(response.getExtra(null)).isNull();
}
@@ -17,7 +17,7 @@ class SearchResponseTest {
@Test
void shouldReturnNullWhenExtraDataKeyIsMissing() {
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
SearchResponse response = new SearchResponse(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
assertThat(response.getExtra("missing")).isNull();
}
@@ -25,8 +25,9 @@ class SearchResponseTest {
@Test
void shouldReturnExtraDataValue() {
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
SearchResponse response = new SearchResponse(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
assertThat(response.id()).isEqualTo(MOCK_ID);
assertThat(response.getExtra("key")).contains("value");
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { Dashboard } from './components/Dashboard'
import { Dashboard } from './components/connections/Dashboard'
function App() {
return (
+2 -3
View File
@@ -1,9 +1,8 @@
import { apiFetch } from "./client";
import type { AuthResponse } from "../types/connection";
import type { SearchRequest } from "../types/search";
import type { PagedSearchResponse, SearchRequest, SearchResponse } from "../types/search";
export const search = (req: SearchRequest) =>
apiFetch<AuthResponse>("/search", {
apiFetch<PagedSearchResponse<SearchResponse>>("/search", {
method: "POST",
body: JSON.stringify(req),
});
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
import { login } from '../api/connections'
import type { LoginRequest } from '../types/connection'
import './ConnectModal.scss'
import { login } from '../../api/connections'
import type { LoginRequest } from '../../types/connection'
import '../ui/Modal.scss'
interface Props {
serviceType: string
@@ -1,10 +1,10 @@
import { useState, useEffect } from "react"
import { getStatuses, logout } from "../api/connections"
import type { ConnectionStatus } from "../types/connection"
import { getStatuses, logout } from "../../api/connections"
import type { ConnectionStatus } from "../../types/connection"
import { ServiceCard } from "./ServiceCard"
import { ConnectModal } from "./ConnectModal"
import "./Dashboard.scss"
import { SearchModal } from "./SearchModal"
import { SearchModal } from "../search/SearchModal"
const SERVICES = [
{ serviceType: 'HOMEBOX', label: 'Homebox', icon: '📦' },
@@ -1,5 +1,5 @@
import type { ConnectionStatus } from '../types/connection'
import { ActionButton } from './ActionButton'
import type { ConnectionStatus } from '../../types/connection'
import { ActionButton } from '../ui/ActionButton'
import './ServiceCard.scss'
interface Props {
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
import './ConnectModal.scss'
import type { SearchRequest } from '../types/search'
import { search } from '../api/searches'
import '../ui/Modal.scss'
import { type PagedSearchResponse, type SearchResponse, type SearchRequest } from '../../types/search'
import { search } from '../../api/searches'
interface Props {
serviceType: string
@@ -12,7 +12,8 @@ interface Props {
}
export function SearchModal({ serviceType, label, appUrl, username, onClose }: Readonly<Props>) {
const [query, setQuery] = useState('');
const [query, setQuery] = useState('')
const [results, setResults] = useState<PagedSearchResponse<SearchResponse> | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const firstInputRef = useRef<HTMLInputElement>(null)
@@ -40,7 +41,10 @@ export function SearchModal({ serviceType, label, appUrl, username, onClose }: R
setLoading(true)
try {
const req: SearchRequest = { appUrl, serviceType, username, query}
await search(req)
const res = await search(req)
console.log(req)
setResults(res)
console.log('results', results)
} catch (err) {
setError(err instanceof Error ? err.message : 'Search failed')
} finally {
@@ -49,21 +53,31 @@ export function SearchModal({ serviceType, label, appUrl, username, onClose }: R
}
return (
<dialog className="modal" ref={dialogRef}>
<div className="modal__header">
<h2 className="modal__title" id="modal-title">Search in {label}</h2>
<button className="modal__close" onClick={onClose} aria-label="Close">×</button>
<dialog className='modal' ref={dialogRef}>
<div className='modal__header'>
<h2 className='modal__title' id='modal-title'>Search in {label}</h2>
<button className='modal__close' onClick={onClose} aria-label='Close'>×</button>
</div>
<form className="modal__form" onSubmit={handleSubmit}>
<div className="modal__field">
<input id="search" className="modal__input"
<form className='modal__form' onSubmit={handleSubmit}>
<div className='modal__field'>
<input id='search' className='modal__input'
value={query} onChange={e => setQuery(e.target.value)} />
</div>
{error && <p className="modal__error">{error}</p>}
<button className="modal__submit" type="submit" disabled={loading}>
{error && <p className='modal__error'>{error}</p>}
<button className='modal__submit' type='submit' disabled={loading}>
Search
</button>
</form>
{results && (
<div className='modal__results'>
<p className='modal__results-count'>{results.totalElements}</p>
{results.content.map((item) => (<div key={item.id} className='modal__result-item'>
<p className='"modal__result-title'>{item.title}</p>
{item.description && <p className='modal__result-desc'>{item.description}</p>}
</div>))}
</div>
)}
</dialog>
)
}
@@ -17,6 +17,8 @@
width: 100%;
max-width: 420px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
&__header {
display: flex;
@@ -87,7 +89,7 @@
font-size: 13px;
color: #ef4444;
padding: 8px 12px;
background: rgba(239, 68, 68, 0.08);
background: rgba(11, 0, 0, 0.499);
border-radius: 6px;
border: 1px solid rgba(239, 68, 68, 0.2);
}
@@ -110,4 +112,44 @@
&:disabled { opacity: 0.6; cursor: not-allowed; }
&:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
}
&--expanded {
max-height: 70vh;
}
&__results {
overflow-y: auto;
flex: 1;
min-height: 0; // required: flex children won't shrink without this
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
&__results-count {
font-size: 12px;
color: var(--text);
margin: 0 0 8px 0;
}
&__result-item {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
}
&__result-title {
font-weight: 500;
font-size: 14px;
color: var(--text-h);
margin: 0;
}
&__result-desc {
font-size: 13px;
color: var(--text);
margin: 4px 0 0 0;
}
}
+18 -1
View File
@@ -2,5 +2,22 @@ export interface SearchRequest {
appUrl: string;
serviceType: string;
username: string;
query: string;
query: string | null;
}
export interface SearchResponse {
id: string;
title: string;
description: string | null;
extraData: Record<string, unknown>;
}
export interface PagedSearchResponse<T> {
content: T[];
page: number;
pageSize: number;
totalElements: number;
first: boolean;
last: boolean;
sort: string;
}